cyberhybridhub/lib/services/questions_api_service.dart
nathanAndrinoid a521a74214
Some checks failed
Admin portal tests / test (push) Has been cancelled
balance controled working spelling and market questions.
2026-06-17 04:20:45 +00:00

293 lines
9.3 KiB
Dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../config/api_config.dart';
import '../models/guess_score_reset_result.dart';
import '../models/guess_score_summary.dart';
import '../models/incoming_question.dart';
import '../models/spelling_progress_reset_result.dart';
import '../models/question_defer_result.dart';
import '../models/question_submit_result.dart';
import 'auth_service.dart';
/// HTTP client for question queue, answers, and deferrals.
class QuestionsApiService {
QuestionsApiService({http.Client? client}) : _client = client ?? http.Client();
final http.Client _client;
/// Ensures login question state is initialized for the signed-in user.
///
/// Returns the first unanswered question when present and always attempts to
/// return persisted [GuessScoreSummary] for this Firebase user when the API
/// includes it in the bootstrap payload.
Future<({IncomingQuestion? question, GuessScoreSummary? score})>
bootstrapOnLogin() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return (question: null, score: null);
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/bootstrap'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'bootstrapOnLogin failed: ${response.statusCode} ${response.body}',
);
return (question: null, score: null);
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? questionJson =
body['question'] as Map<String, dynamic>?;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
return (
question: questionJson == null
? null
: IncomingQuestion.fromJson(questionJson),
score: scoreJson == null
? null
: GuessScoreSummary.fromJson(scoreJson),
);
}
Future<List<IncomingQuestion>> fetchUnanswered() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return <IncomingQuestion>[];
}
final http.Response response = await _client.get(
Uri.parse('$apiBaseUrl/v1/me/questions'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'fetchUnanswered failed: ${response.statusCode} ${response.body}',
);
return <IncomingQuestion>[];
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> raw = body['questions'] as List<dynamic>? ?? <dynamic>[];
return raw
.map(
(dynamic item) =>
IncomingQuestion.fromJson(item as Map<String, dynamic>),
)
.toList();
}
Future<QuestionSubmitResult?> submitAnswer({
required String questionId,
num? answer,
String? answerText,
}) async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final Map<String, dynamic> requestBody = <String, dynamic>{};
if (answerText != null) {
requestBody['answerText'] = answerText;
} else {
requestBody['answer'] = answer ?? 0;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/$questionId/answer'),
headers: _authHeaders(token),
body: jsonEncode(requestBody),
);
if (response.statusCode != 200) {
debugPrint(
'submitAnswer failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
final Map<String, dynamic>? nextQuestionJson =
body['nextQuestion'] as Map<String, dynamic>?;
return QuestionSubmitResult(
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
score: scoreJson == null
? GuessScoreSummary.empty
: GuessScoreSummary.fromJson(scoreJson),
nextQuestion: nextQuestionJson == null
? null
: IncomingQuestion.fromJson(nextQuestionJson),
correctValue: body['correctValue'] as num?,
);
}
/// Runs weighted word selection and creates spelling progress links at tile load.
Future<IncomingQuestion?> prepareSpellingQuestion({
required String questionId,
}) async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/$questionId/spelling/prepare'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'prepareSpellingQuestion failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? questionJson =
body['question'] as Map<String, dynamic>?;
if (questionJson == null) {
return null;
}
return IncomingQuestion.fromJson(questionJson);
}
/// Clears spelling links and familiarity progress, then re-seeds when enabled.
Future<SpellingProgressResetResult?> resetSpellingProgress() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/spelling/reset'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'resetSpellingProgress failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? questionJson =
body['question'] as Map<String, dynamic>?;
return SpellingProgressResetResult(
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
question: questionJson == null
? null
: IncomingQuestion.fromJson(questionJson),
);
}
Future<QuestionDeferResult?> deferQuestion({required String questionId}) async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/$questionId/defer'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'deferQuestion failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? nextQuestionJson =
body['nextQuestion'] as Map<String, dynamic>?;
return QuestionDeferResult(
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
nextQuestion: nextQuestionJson == null
? null
: IncomingQuestion.fromJson(nextQuestionJson),
);
}
Future<GuessScoreSummary?> fetchGuessScoreSummary() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.get(
Uri.parse('$apiBaseUrl/v1/me/questions/score'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'fetchGuessScoreSummary failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
if (scoreJson == null) {
return GuessScoreSummary.empty;
}
return GuessScoreSummary.fromJson(scoreJson);
}
Future<GuessScoreResetResult?> resetGuessScoreSummary() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.post(
Uri.parse('$apiBaseUrl/v1/me/questions/score/reset'),
headers: _authHeaders(token),
);
if (response.statusCode != 200) {
debugPrint(
'resetGuessScoreSummary failed: ${response.statusCode} ${response.body}',
);
return null;
}
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
final Map<String, dynamic>? scoreJson =
body['score'] as Map<String, dynamic>?;
final Map<String, dynamic>? questionJson =
body['question'] as Map<String, dynamic>?;
return GuessScoreResetResult(
score: scoreJson == null
? GuessScoreSummary.empty
: GuessScoreSummary.fromJson(scoreJson),
unansweredCount: (body['unansweredCount'] as num?)?.toInt() ?? 0,
question: questionJson == null
? null
: IncomingQuestion.fromJson(questionJson),
);
}
Map<String, String> _authHeaders(String token) {
return <String, String>{
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
}