cyberhybridhub/server/lib/trading/user_trading_state_db.dart

240 lines
7.4 KiB
Dart

import 'dart:convert';
import 'package:postgres/postgres.dart';
/// Per-user trading worker cursor ([user_trading_state]).
class UserTradingStateDb {
UserTradingStateDb(this._connection);
final Connection _connection;
static const String ingestContextKey = 'ingest_last_fetch';
static const String rulesContextKey = 'rules';
static const String pendingOrdersContextKey = 'pending_orders';
static const String skippedContextKey = 'skipped';
Future<void> ensureExists(String firebaseUid) async {
await _connection.execute(
Sql.named(
'''
INSERT INTO user_trading_state (firebase_uid)
VALUES (@uid)
ON CONFLICT (firebase_uid) DO NOTHING
''',
),
parameters: <String, dynamic>{'uid': firebaseUid},
);
}
Future<Map<String, dynamic>> getContext(String firebaseUid) async {
await ensureExists(firebaseUid);
final Result result = await _connection.execute(
Sql.named(
'SELECT context FROM user_trading_state WHERE firebase_uid = @uid',
),
parameters: <String, dynamic>{'uid': firebaseUid},
);
if (result.isEmpty) {
return <String, dynamic>{};
}
return _readJsonMap(result.first[0]);
}
/// ISO-8601 timestamp of last successful fetch for [dataInputId], or null.
Future<DateTime?> getInputLastFetch(
String firebaseUid,
String dataInputId,
) async {
final Map<String, dynamic> context = await getContext(firebaseUid);
final Map<String, dynamic> ingest = Map<String, dynamic>.from(
context[ingestContextKey] as Map? ?? <String, dynamic>{},
);
final String? raw = ingest[dataInputId] as String?;
if (raw == null || raw.isEmpty) {
return null;
}
return DateTime.parse(raw).toUtc();
}
Future<void> recordInputFetch(
String firebaseUid,
String dataInputId,
DateTime fetchedAt,
) async {
await ensureExists(firebaseUid);
final Map<String, dynamic> context = await getContext(firebaseUid);
final Map<String, dynamic> ingest = Map<String, dynamic>.from(
context[ingestContextKey] as Map? ?? <String, dynamic>{},
);
ingest[dataInputId] = fetchedAt.toUtc().toIso8601String();
context[ingestContextKey] = ingest;
await _connection.execute(
Sql.named(
'''
UPDATE user_trading_state
SET context = @context::jsonb,
last_ingest_at = @last_ingest_at,
updated_at = now()
WHERE firebase_uid = @uid
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'context': jsonEncode(context),
'last_ingest_at': fetchedAt.toUtc(),
},
);
}
Future<Map<String, dynamic>?> getRuleState(
String firebaseUid,
String ruleId,
) async {
final Map<String, dynamic> context = await getContext(firebaseUid);
final Map<String, dynamic> rules = Map<String, dynamic>.from(
context[rulesContextKey] as Map? ?? <String, dynamic>{},
);
final Map? raw = rules[ruleId] as Map?;
if (raw == null) {
return null;
}
return Map<String, dynamic>.from(raw);
}
Future<DateTime?> getRuleLastFiredAt(
String firebaseUid,
String ruleId,
) async {
final Map<String, dynamic>? state = await getRuleState(firebaseUid, ruleId);
final String? raw = state?['last_fired_at'] as String?;
if (raw == null || raw.isEmpty) {
return null;
}
return DateTime.parse(raw).toUtc();
}
Future<void> setRuleState({
required String firebaseUid,
required String ruleId,
required Map<String, dynamic> state,
}) async {
await ensureExists(firebaseUid);
final Map<String, dynamic> context = await getContext(firebaseUid);
final Map<String, dynamic> rules = Map<String, dynamic>.from(
context[rulesContextKey] as Map? ?? <String, dynamic>{},
);
rules[ruleId] = state;
context[rulesContextKey] = rules;
await _writeContext(firebaseUid, context, touchEvalAt: true);
}
Future<List<Map<String, dynamic>>> listPendingOrders(
String firebaseUid,
) async {
final Map<String, dynamic> context = await getContext(firebaseUid);
final List<dynamic> raw =
context[pendingOrdersContextKey] as List<dynamic>? ?? <dynamic>[];
return raw
.whereType<Map>()
.map((Map<dynamic, dynamic> m) => Map<String, dynamic>.from(m))
.toList();
}
Future<void> addPendingOrder({
required String firebaseUid,
required Map<String, dynamic> order,
}) async {
await ensureExists(firebaseUid);
final Map<String, dynamic> context = await getContext(firebaseUid);
final List<dynamic> existing =
context[pendingOrdersContextKey] as List<dynamic>? ?? <dynamic>[];
final List<Map<String, dynamic>> orders = <Map<String, dynamic>>[
...existing.whereType<Map>().map(
(Map<dynamic, dynamic> m) => Map<String, dynamic>.from(m),
),
order,
];
context[pendingOrdersContextKey] = orders;
await _writeContext(firebaseUid, context, touchEvalAt: true);
}
Future<void> removePendingOrder({
required String firebaseUid,
required String clientOrderId,
}) async {
await ensureExists(firebaseUid);
final Map<String, dynamic> context = await getContext(firebaseUid);
final List<dynamic> existing =
context[pendingOrdersContextKey] as List<dynamic>? ?? <dynamic>[];
final List<Map<String, dynamic>> kept = existing
.whereType<Map>()
.map((Map<dynamic, dynamic> m) => Map<String, dynamic>.from(m))
.where((Map<String, dynamic> m) =>
m['client_order_id'] != clientOrderId)
.toList();
context[pendingOrdersContextKey] = kept;
await _writeContext(firebaseUid, context, touchEvalAt: false);
}
Future<void> recordSkip({
required String firebaseUid,
required String ruleId,
required String questionId,
required DateTime at,
}) async {
await ensureExists(firebaseUid);
final Map<String, dynamic> context = await getContext(firebaseUid);
final List<dynamic> existing =
context[skippedContextKey] as List<dynamic>? ?? <dynamic>[];
final List<Map<String, dynamic>> skipped = <Map<String, dynamic>>[
...existing.whereType<Map>().map(
(Map<dynamic, dynamic> m) => Map<String, dynamic>.from(m),
),
<String, dynamic>{
'rule_id': ruleId,
'question_id': questionId,
'at': at.toUtc().toIso8601String(),
},
];
context[skippedContextKey] = skipped;
await _writeContext(firebaseUid, context, touchEvalAt: false);
}
Future<void> _writeContext(
String firebaseUid,
Map<String, dynamic> context, {
required bool touchEvalAt,
}) async {
final String setEval = touchEvalAt ? ', last_eval_at = now()' : '';
await _connection.execute(
Sql.named(
'''
UPDATE user_trading_state
SET context = @context::jsonb,
updated_at = now()
$setEval
WHERE firebase_uid = @uid
''',
),
parameters: <String, dynamic>{
'uid': firebaseUid,
'context': jsonEncode(context),
},
);
}
Map<String, dynamic> _readJsonMap(Object? value) {
if (value is Map<String, dynamic>) {
return value;
}
if (value is Map) {
return Map<String, dynamic>.from(value);
}
if (value == null) {
return <String, dynamic>{};
}
return jsonDecode(value.toString()) as Map<String, dynamic>;
}
}