cyberhybridhub/server/lib/question_source_preferences.dart
nathanAndrinoid 4459852f83
Some checks failed
Admin portal tests / test (push) Has been cancelled
spelling
2026-06-05 03:22:35 +00:00

216 lines
6.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// Canonical question source definitions and merge/validation helpers.
class QuestionSourcePreferenceDefinition {
const QuestionSourcePreferenceDefinition({
required this.id,
required this.label,
required this.description,
this.defaultEnabled = true,
this.defaultRatio = 50,
this.comingSoon = false,
});
final String id;
final String label;
final String description;
final bool defaultEnabled;
final int defaultRatio;
final bool comingSoon;
}
class ResolvedQuestionSourcePreference {
const ResolvedQuestionSourcePreference({
required this.id,
required this.label,
required this.description,
required this.enabled,
required this.ratio,
required this.comingSoon,
});
final String id;
final String label;
final String description;
final bool enabled;
final int ratio;
final bool comingSoon;
Map<String, dynamic> toJson() {
return <String, dynamic>{
'id': id,
'label': label,
'description': description,
'enabled': enabled,
'ratio': ratio,
'comingSoon': comingSoon,
};
}
Map<String, dynamic> toStoredJson() {
return <String, dynamic>{
'id': id,
'enabled': enabled,
'ratio': ratio,
};
}
}
class QuestionSourceValidationException implements Exception {
QuestionSourceValidationException(this.message);
final String message;
@override
String toString() => message;
}
abstract final class QuestionSourcePreferences {
static const List<QuestionSourcePreferenceDefinition> definitions =
<QuestionSourcePreferenceDefinition>[
QuestionSourcePreferenceDefinition(
id: 'market_history',
label: 'Market question',
description: 'Prospective price-move guessing game',
),
QuestionSourcePreferenceDefinition(
id: 'spelling',
label: 'Spelling',
description: 'Spell-check quiz from curated word lists',
),
];
static List<ResolvedQuestionSourcePreference> mergeStored(
List<Map<String, dynamic>>? stored,
) {
final Map<String, Map<String, dynamic>> byId =
<String, Map<String, dynamic>>{
if (stored != null)
for (final Map<String, dynamic> row in stored) row['id']! as String: row,
};
return definitions
.map((QuestionSourcePreferenceDefinition definition) {
final Map<String, dynamic>? saved = byId[definition.id];
final bool enabled = definition.comingSoon
? false
: saved?['enabled'] as bool? ?? definition.defaultEnabled;
final int ratio =
(saved?['ratio'] as num?)?.round().clamp(0, 100) ??
definition.defaultRatio;
return ResolvedQuestionSourcePreference(
id: definition.id,
label: definition.label,
description: definition.description,
enabled: enabled,
ratio: ratio,
comingSoon: definition.comingSoon,
);
})
.toList(growable: false);
}
static List<Map<String, dynamic>> parsePutBody(Map<String, dynamic> body) {
final Object? rawSources = body['sources'];
if (rawSources is! List<dynamic>) {
throw QuestionSourceValidationException('Expected sources array.');
}
final Map<String, QuestionSourcePreferenceDefinition> known =
<String, QuestionSourcePreferenceDefinition>{
for (final QuestionSourcePreferenceDefinition definition in definitions)
definition.id: definition,
};
final List<Map<String, dynamic>> parsed = <Map<String, dynamic>>[];
for (final Object? entry in rawSources) {
if (entry is! Map<String, dynamic>) {
throw QuestionSourceValidationException('Invalid source entry.');
}
final String? id = entry['id'] as String?;
if (id == null || !known.containsKey(id)) {
throw QuestionSourceValidationException('Unknown source id: $id');
}
final QuestionSourcePreferenceDefinition definition = known[id]!;
final bool enabled = definition.comingSoon
? false
: entry['enabled'] as bool? ?? definition.defaultEnabled;
if (definition.comingSoon && entry['enabled'] == true) {
throw QuestionSourceValidationException(
'Source $id is not available yet.',
);
}
final int ratio =
(entry['ratio'] as num?)?.round().clamp(0, 100) ??
definition.defaultRatio;
parsed.add(<String, dynamic>{
'id': id,
'enabled': enabled,
'ratio': ratio,
});
}
if (parsed.length != definitions.length) {
throw QuestionSourceValidationException(
'Provide every known question source.',
);
}
final bool hasEnabled = parsed.any((Map<String, dynamic> row) {
final QuestionSourcePreferenceDefinition definition =
known[row['id']! as String]!;
return row['enabled'] == true && !definition.comingSoon;
});
if (!hasEnabled) {
throw QuestionSourceValidationException(
'Enable at least one question source.',
);
}
return parsed;
}
/// Effective share (0–100) for each enabled source; empty when none enabled.
static Map<String, int> effectiveMix(
List<ResolvedQuestionSourcePreference> sources,
) {
final Iterable<ResolvedQuestionSourcePreference> enabled = sources.where(
(ResolvedQuestionSourcePreference s) => s.enabled && !s.comingSoon,
);
final int totalWeight = enabled.fold<int>(
0,
(int sum, ResolvedQuestionSourcePreference s) => sum + s.ratio,
);
if (totalWeight <= 0) {
return <String, int>{};
}
return <String, int>{
for (final ResolvedQuestionSourcePreference source in enabled)
source.id: ((source.ratio / totalWeight) * 100).round(),
};
}
static bool isEnabled(
List<ResolvedQuestionSourcePreference> resolved,
String sourceId,
) {
for (final ResolvedQuestionSourcePreference source in resolved) {
if (source.id == sourceId) {
return source.enabled && !source.comingSoon;
}
}
return false;
}
static Map<String, dynamic> toApiPayload({
required List<ResolvedQuestionSourcePreference> sources,
DateTime? updatedAt,
}) {
return <String, dynamic>{
'sources': sources
.map((ResolvedQuestionSourcePreference s) => s.toJson())
.toList(),
if (updatedAt != null) 'updatedAt': updatedAt.toUtc().toIso8601String(),
};
}
}