Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
RolePlaySessionDeletionService
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
2 / 2
8
100.00% covered (success)
100.00%
1 / 1
 delete
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
6
 getThreshold
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Http\Services\RolePlay;
4
5use App\Http\Models\Auth\User;
6use App\Http\Models\Parameter;
7use App\Http\Models\RolePlayConversations;
8use App\Http\Models\RolePlayProjects;
9use App\Http\Models\UserRolePlayProgression;
10use Illuminate\Auth\Access\AuthorizationException;
11use Illuminate\Support\Facades\Log;
12
13/**
14 * Deletes a single short roleplay session (soft-delete) and recomputes
15 * all downstream aggregates:
16 *
17 *   - The owning user's UserRolePlayProgression for the session's
18 *     call_type (drops the entry, replays EMA).
19 *   - The persona's average_score.
20 *
21 * Deletion is gated by the CMC-configured threshold parameter
22 * `role_play_min_session_duration_seconds` (default 5). Sessions at or
23 * above the threshold cannot be deleted through this endpoint.
24 *
25 * Daily usage metrics and UserInfo counters are left intact — the
26 * session is soft-deleted and UsageTrait::getUsage() queries those rows
27 * with `withTrashed()` so daily numbers remain stable.
28 */
29class RolePlaySessionDeletionService
30{
31    use RolePlayDeletionAuthTrait;
32
33    public const PARAMETER_NAME = 'role_play_min_session_duration_seconds';
34
35    public const DEFAULT_THRESHOLD_SECONDS = 5;
36
37    /**
38     * @return array{progression: array<int, UserRolePlayProgression>, persona_average_score: float|null}
39     *
40     * @throws AuthorizationException
41     */
42    public function delete(User $actor, RolePlayConversations $session): array
43    {
44        $ownerId = (string) $session->user_id;
45        $owner = User::find($ownerId);
46        $ownerCompanyId = $owner?->company_id ? (string) $owner->company_id : null;
47
48        $this->assertCanActOn($actor, $ownerId, $ownerCompanyId);
49
50        $threshold = $this->getThreshold();
51        $duration = (int) ($session->duration ?? 0);
52        if ($duration >= $threshold) {
53            throw new AuthorizationException(sprintf(
54                'Only sessions shorter than %d seconds can be deleted.',
55                $threshold,
56            ));
57        }
58
59        $sessionId = (string) $session->id;
60        $projectId = (string) $session->project_id;
61
62        $session->delete();
63
64        $progressions = UserRolePlayProgression::where('user_id', $ownerId)->get();
65        foreach ($progressions as $progression) {
66            $progression->dropEntriesAndRecompute([$sessionId], []);
67        }
68
69        $personaAverage = null;
70        $persona = RolePlayProjects::find($projectId);
71        if ($persona) {
72            $average = RolePlayConversations::where('project_id', $projectId)
73                ->where('status', 'done')
74                ->avg('score');
75            $personaAverage = $average !== null ? (float) $average : 0.0;
76            $persona->average_score = $personaAverage;
77            $persona->save();
78        }
79
80        Log::info('Roleplay session deleted', [
81            'session_id' => $sessionId,
82            'persona_id' => $projectId,
83            'owner_id' => $ownerId,
84            'actor_id' => (string) $actor->id,
85            'duration' => $duration,
86        ]);
87
88        return [
89            'progression' => $progressions->values()->all(),
90            'persona_average_score' => $personaAverage,
91        ];
92    }
93
94    public function getThreshold(): int
95    {
96        $value = Parameter::where('name', self::PARAMETER_NAME)->first()?->value;
97
98        if ($value === null) {
99            return self::DEFAULT_THRESHOLD_SECONDS;
100        }
101
102        return (int) $value;
103    }
104}