adminToken(); $qnId = $this->fixture()->questionnaireId; $create = $this->api()->request('POST', 'program_sessions', [ 'name' => 'Baseline', 'description' => 'First visit', 'sessionNumber' => 1, 'orderIndex' => 0, 'isActive' => true, 'questionnaires' => [ ['questionnaireID' => $qnId, 'orderIndex' => 0], ], ], ['Authorization' => 'Bearer ' . $token]); $this->assertApiOk($create); $sessionID = $create['data']['session']['sessionID'] ?? ''; $this->assertNotSame('', $sessionID); $this->assertSame('Baseline', $create['data']['session']['name']); $this->assertCount(1, $create['data']['session']['questionnaires']); $list = $this->api()->request('GET', 'program_sessions', null, [ 'Authorization' => 'Bearer ' . $token, ]); $this->assertApiOk($list); $this->assertCount(1, $list['data']['sessions']); $this->assertFalse($list['data']['settings']['autoRepeatSessions'] ?? true); $update = $this->api()->request('PUT', 'program_sessions', [ 'sessionID' => $sessionID, 'name' => 'Baseline updated', 'sessionNumber' => 2, 'questionnaires' => [], ], ['Authorization' => 'Bearer ' . $token]); $this->assertApiOk($update); $this->assertSame('Baseline updated', $update['data']['session']['name']); $this->assertSame([], $update['data']['session']['questionnaires']); $mobile = $this->coachMobileLogin(); $appRes = $this->api()->request('GET', 'app_questionnaires', null, [ 'Authorization' => 'Bearer ' . $mobile['token'], ], ['sessions' => '1']); $this->assertApiOk($appRes); $this->assertCount(1, $appRes['data']['sessions']); $this->assertSame($sessionID, $appRes['data']['sessions'][0]['sessionID']); $this->assertArrayHasKey('settings', $appRes['data']); $delete = $this->api()->request('DELETE', 'program_sessions', [ 'sessionID' => $sessionID, ], ['Authorization' => 'Bearer ' . $token]); $this->assertApiOk($delete); $empty = $this->api()->request('GET', 'program_sessions', null, [ 'Authorization' => 'Bearer ' . $token, ]); $this->assertApiOk($empty); $this->assertSame([], $empty['data']['sessions']); } public function testAutoRepeatAdvancesCycleAfterLastSessionCompleted(): void { $adminToken = $this->adminToken(); $qnId = $this->fixture()->questionnaireId; $clientCode = $this->fixture()->clientCode; $this->assertApiOk($this->api()->request('PATCH', 'program_sessions', [ 'autoRepeatSessions' => true, ], ['Authorization' => 'Bearer ' . $adminToken])); $session1 = $this->api()->request('POST', 'program_sessions', [ 'name' => 'Session 1', 'sessionNumber' => 1, 'orderIndex' => 0, 'isActive' => true, 'questionnaires' => [['questionnaireID' => $qnId, 'orderIndex' => 0]], ], ['Authorization' => 'Bearer ' . $adminToken]); $this->assertApiOk($session1); $session2 = $this->api()->request('POST', 'program_sessions', [ 'name' => 'Session 2', 'sessionNumber' => 2, 'orderIndex' => 1, 'isActive' => true, 'questionnaires' => [['questionnaireID' => $qnId, 'orderIndex' => 0]], ], ['Authorization' => 'Bearer ' . $adminToken]); $this->assertApiOk($session2); $login = $this->coachMobileLogin(); $submit = $this->submitQuestionnaireAs( $login['token'], $qnId, $clientCode, [['questionID' => $this->fixture()->questionShortId, 'answerOptionKey' => 'yes']], ); $this->assertApiOk($submit); $this->assertTrue($submit['data']['cycleAdvanced'] ?? false); $this->assertSame(2, $submit['data']['cycleNumber'] ?? 0); $clientsRes = $this->api()->withMobileToken($login['token'], 'GET', 'app_questionnaires', null, [ 'clients' => '1', ]); $this->assertApiOk($clientsRes); $plain = qdb_decrypt_sensitive_envelope($clientsRes['data'], $login['token']); $payload = json_decode($plain, true, 512, JSON_THROW_ON_ERROR); $client = null; foreach ($payload['clients'] as $row) { if (($row['clientCode'] ?? '') === $clientCode) { $client = $row; break; } } $this->assertNotNull($client); $progress = $client['programProgress']; $this->assertSame(2, $progress['cycleNumber']); $this->assertSame(2, $progress['totalSessions']); $this->assertSame(0, $progress['sessionsCompletedInCycle']); $this->assertTrue($progress['autoRepeatSessions']); $this->assertFalse($progress['programComplete']); } public function testAutoRepeatDisabledKeepsProgramComplete(): void { $adminToken = $this->adminToken(); $qnId = $this->fixture()->questionnaireId; $clientCode = $this->fixture()->clientCode; $this->assertApiOk($this->api()->request('PATCH', 'program_sessions', [ 'autoRepeatSessions' => false, ], ['Authorization' => 'Bearer ' . $adminToken])); $this->assertApiOk($this->api()->request('POST', 'program_sessions', [ 'name' => 'Only session', 'sessionNumber' => 1, 'orderIndex' => 0, 'isActive' => true, 'questionnaires' => [['questionnaireID' => $qnId, 'orderIndex' => 0]], ], ['Authorization' => 'Bearer ' . $adminToken])); $login = $this->coachMobileLogin(); $submit = $this->submitQuestionnaireAs( $login['token'], $qnId, $clientCode, [['questionID' => $this->fixture()->questionShortId, 'answerOptionKey' => 'yes']], ); $this->assertApiOk($submit); $this->assertFalse($submit['data']['cycleAdvanced'] ?? true); $this->assertSame(1, $submit['data']['cycleNumber'] ?? 0); $clientsRes = $this->api()->withMobileToken($login['token'], 'GET', 'app_questionnaires', null, [ 'clients' => '1', ]); $this->assertApiOk($clientsRes); $plain = qdb_decrypt_sensitive_envelope($clientsRes['data'], $login['token']); $payload = json_decode($plain, true, 512, JSON_THROW_ON_ERROR); $progress = $payload['clients'][0]['programProgress']; $this->assertTrue($progress['cycleComplete']); $this->assertTrue($progress['programComplete']); $this->assertFalse($progress['autoRepeatSessions']); } public function testRejectUnknownQuestionnaire(): void { $token = $this->adminToken(); $res = $this->api()->request('POST', 'program_sessions', [ 'name' => 'Invalid', 'questionnaires' => [ ['questionnaireID' => 'does-not-exist'], ], ], ['Authorization' => 'Bearer ' . $token]); $this->assertApiError($res, 'INVALID_FIELD', 400); } public function testCoachCannotCreateSession(): void { $mobile = $this->coachMobileLogin(); $res = $this->api()->request('POST', 'program_sessions', [ 'name' => 'Blocked', ], ['Authorization' => 'Bearer ' . $mobile['token']]); $this->assertApiError($res, 'FORBIDDEN', 403); } }