Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
5 / 5
CRAP
100.00% covered (success)
100.00%
1 / 1
UserCompanyRolePlayController
100.00% covered (success)
100.00%
80 / 80
100.00% covered (success)
100.00%
5 / 5
13
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 show
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 startSession
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
6
 mySessions
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
2
 myProgression
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Http\Controllers\v2\RolePlay;
4
5use App\Http\Controllers\Controller;
6use App\Http\Models\CompanyRolePlayProject;
7use App\Http\Models\RolePlayConversations;
8use App\Http\Resources\v2\CompanyRolePlayProjectResource;
9use App\Http\Resources\v2\RolePlayConversationAdminResource;
10use App\Http\Services\CompanyRolePlaySessionService;
11use App\Http\Services\RolePlay\RolePlayCallTypeSettingsService;
12use App\Http\Services\RolePlayPromptBuilderService;
13use Illuminate\Http\JsonResponse;
14use Illuminate\Http\Request;
15
16/**
17 * User-facing endpoints that sit on top of the corporate persona feature
18 * but are called from the end-user roleplay app (not the admin panel):
19 *  - Inspect a corporate persona the user has access to
20 *  - Start a **direct** call against a corporate persona (no clone)
21 *  - Review the user's own sessions / progression on a given corporate persona
22 *
23 * All visibility/authorisation is enforced via the
24 * {@see CompanyRolePlayProject::scopeAvailableToUser()} scope so a user who
25 * is not in an assigned group (or explicit `assigned_users` list) simply
26 * gets a 404 for anything they are not supposed to see.
27 */
28class UserCompanyRolePlayController extends Controller
29{
30    public function __construct(
31        private readonly CompanyRolePlaySessionService $sessionService,
32    ) {}
33
34    /**
35     * Show a corporate persona to a user. Returns 404 if the persona isn't
36     * available to the user per `availableToUser` scope.
37     */
38    public function show(Request $request, string $id)
39    {
40        $project = CompanyRolePlayProject::availableToUser($request->user())->find($id);
41
42        if (! $project) {
43            return response()->json(['status' => 'error', 'message' => 'Company project not found'], 404);
44        }
45
46        return new CompanyRolePlayProjectResource($project);
47    }
48
49    /**
50     * Start a **direct** call session against a corporate persona.
51     *
52     * Creates a {@see RolePlayConversations} row with `project_id = null`
53     * and `company_project_id = {id}`. The observer stamps `company_id`
54     * from the authenticated caller so the row slots into company-level
55     * aggregates. Rejects the call when the persona has
56     * `allow_direct_calls = false`.
57     */
58    public function startSession(
59        Request $request,
60        string $id,
61        RolePlayPromptBuilderService $promptBuilder,
62        RolePlayCallTypeSettingsService $callTypeSettings,
63    ): JsonResponse {
64        $user = $request->user();
65
66        $project = CompanyRolePlayProject::availableToUser($user)->find($id);
67        if (! $project) {
68            return response()->json(['status' => 'error', 'message' => 'Company project not found'], 404);
69        }
70
71        $allowDirect = $project->allow_direct_calls;
72        if ($allowDirect === null) {
73            // Phase 1 default: direct calls are allowed unless an admin
74            // opts out explicitly. The legacy column is absent on
75            // pre-migration records.
76            $allowDirect = true;
77        }
78
79        if (! $allowDirect) {
80            return response()->json([
81                'status' => 'error',
82                'message' => 'This persona does not allow direct calls',
83            ], 403);
84        }
85
86        $request->validate([
87            'icp' => 'required',
88            'agent' => 'nullable',
89        ]);
90
91        $icpInput = is_string($request->input('icp'))
92            ? json_decode($request->input('icp'), true)
93            : (array) $request->input('icp');
94
95        $prompt = $promptBuilder->buildPromptForIcp($project->toArray(), $icpInput ?? []);
96        $prompt = str_replace('{jobTitle}', $icpInput['target_job_title'] ?? '', $prompt);
97
98        $resolvedTarget = $callTypeSettings->resolve($user, $project->type);
99
100        $conversation = RolePlayConversations::create([
101            'user_id' => $user->id,
102            'project_id' => null,
103            'company_project_id' => (string) $project->id,
104            'company_id' => $user->company_id,
105            'icp' => is_string($request->input('icp')) ? $request->input('icp') : json_encode($icpInput),
106            'prompt' => $prompt,
107            'agent' => $request->input('agent'),
108            'duration' => 0,
109            'status' => 'created',
110            'target_duration_seconds_at_call' => (int) $resolvedTarget['target_duration_seconds'],
111        ]);
112
113        return response()->json([
114            'status' => 'success',
115            'data' => [
116                'session' => $conversation,
117                'prompt' => $prompt,
118            ],
119        ]);
120    }
121
122    /**
123     * The caller's own sessions on a single corporate persona (direct + clone).
124     */
125    public function mySessions(Request $request, string $id): JsonResponse
126    {
127        $user = $request->user();
128        $project = CompanyRolePlayProject::availableToUser($user)->find($id);
129        if (! $project) {
130            return response()->json(['status' => 'error', 'message' => 'Company project not found'], 404);
131        }
132
133        $filters = $request->only(['status', 'min_score', 'max_score', 'date_from', 'date_to', 'per_page']);
134        $paginator = $this->sessionService->sessionsForUserOnCorporatePersona(
135            (string) $user->id,
136            $id,
137            $filters,
138        );
139
140        return response()->json([
141            'status' => 'success',
142            'data' => RolePlayConversationAdminResource::collection($paginator->getCollection())->toArray($request),
143            'meta' => [
144                'current_page' => $paginator->currentPage(),
145                'per_page' => $paginator->perPage(),
146                'total' => $paginator->total(),
147                'last_page' => $paginator->lastPage(),
148            ],
149        ]);
150    }
151
152    /**
153     * The caller's progression summary (avg score + weekly timeseries) on
154     * a single corporate persona.
155     */
156    public function myProgression(Request $request, string $id): JsonResponse
157    {
158        $user = $request->user();
159        $project = CompanyRolePlayProject::availableToUser($user)->find($id);
160        if (! $project) {
161            return response()->json(['status' => 'error', 'message' => 'Company project not found'], 404);
162        }
163
164        $weeks = max(1, (int) $request->input('weeks', 12));
165        $summary = $this->sessionService->userProgressionOnCorporatePersona(
166            (string) $user->id,
167            $id,
168            $weeks,
169        );
170
171        return response()->json([
172            'status' => 'success',
173            'data' => $summary,
174        ]);
175    }
176}