cyberhybridhub/server/lib/trading/prospective_guess_assignments_db.dart

258 lines
7.2 KiB
Dart

import 'package:postgres/postgres.dart';
import 'market_history_session_slot.dart';
/// Persisted guess slot assignment (one row per user, slot pair, and symbol).
class ProspectiveGuessAssignmentsDb {
ProspectiveGuessAssignmentsDb(this._connection);
final Connection _connection;
static const String statusPending = 'pending';
static const String statusAnswered = 'answered';
/// Question id for a pending assignment whose question is still unanswered.
Future<String?> findPendingQuestionId(String firebaseUid) async {
final Result result = await _connection.execute(
Sql.named(
'''
SELECT a.question_id
FROM market_history_prospective_assignments a
INNER JOIN questions q ON q.id = a.question_id
WHERE a.assigned_user_id = @uid
AND a.status = @pending
AND q.user_response IS NULL
ORDER BY a.created_at ASC
LIMIT 1
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'pending': statusPending,
},
);
if (result.isEmpty) {
return null;
}
return result.first[0].toString();
}
Future<bool> hasPendingAssignmentForSlotPair({
required String firebaseUid,
required DateTime olderSlotStart,
required DateTime newerSlotStart,
}) async {
final Result result = await _connection.execute(
Sql.named(
'''
SELECT 1
FROM market_history_prospective_assignments a
INNER JOIN questions q ON q.id = a.question_id
WHERE a.assigned_user_id = @uid
AND a.older_slot_start = @older
AND a.newer_slot_start = @newer
AND a.status = @pending
AND q.user_response IS NULL
LIMIT 1
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'older': _slot(olderSlotStart),
'newer': _slot(newerSlotStart),
'pending': statusPending,
},
);
return result.isNotEmpty;
}
Future<bool> hasAssignmentForSymbolSlotPair({
required String firebaseUid,
required DateTime olderSlotStart,
required DateTime newerSlotStart,
required String symbol,
}) async {
final Result result = await _connection.execute(
Sql.named(
'''
SELECT 1
FROM market_history_prospective_assignments
WHERE assigned_user_id = @uid
AND older_slot_start = @older
AND newer_slot_start = @newer
AND symbol = @symbol
LIMIT 1
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'older': _slot(olderSlotStart),
'newer': _slot(newerSlotStart),
'symbol': symbol,
},
);
return result.isNotEmpty;
}
Future<Set<String>> assignedSymbolsForSlotPair({
required String firebaseUid,
required DateTime olderSlotStart,
required DateTime newerSlotStart,
}) async {
final Result result = await _connection.execute(
Sql.named(
'''
SELECT symbol
FROM market_history_prospective_assignments
WHERE assigned_user_id = @uid
AND older_slot_start = @older
AND newer_slot_start = @newer
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'older': _slot(olderSlotStart),
'newer': _slot(newerSlotStart),
},
);
return result.map((ResultRow row) => row[0]! as String).toSet();
}
Future<Set<String>> answeredSymbolsForSlotPair({
required String firebaseUid,
required DateTime olderSlotStart,
required DateTime newerSlotStart,
}) async {
final Result result = await _connection.execute(
Sql.named(
'''
SELECT symbol
FROM market_history_prospective_assignments
WHERE assigned_user_id = @uid
AND older_slot_start = @older
AND newer_slot_start = @newer
AND status = @answered
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'older': _slot(olderSlotStart),
'newer': _slot(newerSlotStart),
'answered': statusAnswered,
},
);
return result.map((ResultRow row) => row[0]! as String).toSet();
}
/// Inserts a pending row when none exists for this user/slot pair/symbol.
///
/// Returns false when an assignment already exists (unique constraint).
Future<bool> insertPendingIfAbsent({
required String firebaseUid,
required DateTime olderSlotStart,
required DateTime newerSlotStart,
required String symbol,
required String prospectiveQuestionId,
required String questionId,
}) async {
final Result result = await _connection.execute(
Sql.named(
'''
INSERT INTO market_history_prospective_assignments (
assigned_user_id,
older_slot_start,
newer_slot_start,
symbol,
prospective_question_id,
question_id,
status
) VALUES (
@uid,
@older,
@newer,
@symbol,
@prospective_question_id::uuid,
@question_id::uuid,
@pending
)
ON CONFLICT (assigned_user_id, older_slot_start, newer_slot_start, symbol)
DO NOTHING
RETURNING id
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'older': _slot(olderSlotStart),
'newer': _slot(newerSlotStart),
'symbol': symbol,
'prospective_question_id': prospectiveQuestionId,
'question_id': questionId,
'pending': statusPending,
},
);
return result.isNotEmpty;
}
Future<void> insertPending({
required String firebaseUid,
required DateTime olderSlotStart,
required DateTime newerSlotStart,
required String symbol,
required String prospectiveQuestionId,
required String questionId,
}) async {
final bool inserted = await insertPendingIfAbsent(
firebaseUid: firebaseUid,
olderSlotStart: olderSlotStart,
newerSlotStart: newerSlotStart,
symbol: symbol,
prospectiveQuestionId: prospectiveQuestionId,
questionId: questionId,
);
if (!inserted) {
throw StateError(
'Assignment already exists for $firebaseUid $symbol '
'${_slot(olderSlotStart).toIso8601String()}',
);
}
}
Future<void> markAnsweredByQuestionId({
required String questionId,
required DateTime answeredAt,
}) async {
await _connection.execute(
Sql.named(
'''
UPDATE market_history_prospective_assignments
SET status = @answered,
answered_at = @answered_at
WHERE question_id = @question_id::uuid
AND status = @pending
''',
),
parameters: <String, dynamic>{
'question_id': questionId,
'answered': statusAnswered,
'answered_at': answeredAt.toUtc(),
'pending': statusPending,
},
);
}
Future<void> deleteAllForUser(String firebaseUid) async {
await _connection.execute(
Sql.named(
'''
DELETE FROM market_history_prospective_assignments
WHERE assigned_user_id = @uid
''',
),
parameters: <String, dynamic>{'uid': firebaseUid},
);
}
static DateTime _slot(DateTime value) =>
MarketHistorySessionSlot.slotStartContaining(value.toUtc());
}