namespace NeonSprawl.Server.Game.Factions; /// PostgreSQL-backed append-only reputation delta audit log (NEO-135). public sealed class PostgresReputationDeltaStore(Npgsql.NpgsqlDataSource dataSource) : IReputationDeltaStore { /// public bool TryAppend(ReputationDeltaRow row) { if (!IsValidRow(row)) { return false; } var normalized = row with { Id = row.Id.Trim(), PlayerId = FactionStandingIds.NormalizePlayerId(row.PlayerId), FactionId = FactionStandingIds.NormalizeFactionId(row.FactionId), SourceKind = row.SourceKind.Trim(), SourceId = row.SourceId.Trim(), }; PostgresReputationDeltaBootstrap.EnsureSchema(dataSource); using var conn = dataSource.OpenConnection(); using var cmd = new Npgsql.NpgsqlCommand( """ INSERT INTO reputation_delta_audit ( id, player_id, faction_id, delta_amount, resulting_standing, source_kind, source_id, recorded_at) VALUES (@id, @pid, @fid, @delta, @result, @kind, @source_id, @recorded) ON CONFLICT (id) DO NOTHING; """, conn); cmd.Parameters.AddWithValue("id", normalized.Id); cmd.Parameters.AddWithValue("pid", normalized.PlayerId); cmd.Parameters.AddWithValue("fid", normalized.FactionId); cmd.Parameters.AddWithValue("delta", normalized.DeltaAmount); cmd.Parameters.AddWithValue("result", normalized.ResultingStanding); cmd.Parameters.AddWithValue("kind", normalized.SourceKind); cmd.Parameters.AddWithValue("source_id", normalized.SourceId); cmd.Parameters.AddWithValue("recorded", normalized.RecordedAt); return cmd.ExecuteNonQuery() == 1; } /// public IReadOnlyList GetDeltasForPlayer(string playerId, int? limit = null) { var player = FactionStandingIds.NormalizePlayerId(playerId); if (player.Length == 0) { return []; } PostgresReputationDeltaBootstrap.EnsureSchema(dataSource); using var conn = dataSource.OpenConnection(); var sql = limit is > 0 ? """ SELECT id, player_id, faction_id, delta_amount, resulting_standing, source_kind, source_id, recorded_at FROM reputation_delta_audit WHERE player_id = @pid ORDER BY recorded_at ASC, id ASC LIMIT @lim; """ : """ SELECT id, player_id, faction_id, delta_amount, resulting_standing, source_kind, source_id, recorded_at FROM reputation_delta_audit WHERE player_id = @pid ORDER BY recorded_at ASC, id ASC; """; using var cmd = new Npgsql.NpgsqlCommand(sql, conn); cmd.Parameters.AddWithValue("pid", player); if (limit is > 0) { cmd.Parameters.AddWithValue("lim", limit.Value); } var rows = new List(); using var reader = cmd.ExecuteReader(); while (reader.Read()) { rows.Add(new ReputationDeltaRow( reader.GetString(0), reader.GetString(1), reader.GetString(2), reader.GetInt32(3), reader.GetInt32(4), reader.GetString(5), reader.GetString(6), reader.GetFieldValue(7))); } return rows; } private static bool IsValidRow(ReputationDeltaRow row) => row.Id.Trim().Length > 0 && FactionStandingIds.NormalizePlayerId(row.PlayerId).Length > 0 && FactionStandingIds.NormalizeFactionId(row.FactionId).Length > 0 && row.SourceKind.Trim().Length > 0 && row.SourceId.Trim().Length > 0; }