feat(backend): add communication, tester, admin and middleman modules
Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
using SurrealDb.Net;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Infrastructure.Persistence;
|
||||
|
||||
public sealed class NotificationService : INotificationService
|
||||
{
|
||||
private const string SelectFields =
|
||||
"type::string(id) AS id, type, payload, read_at, created_at";
|
||||
|
||||
private static readonly string[] StaffRoles = ["operator", "manager", "admin"];
|
||||
|
||||
private readonly ISurrealDbClient _client;
|
||||
|
||||
public NotificationService(ISurrealDbClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public async Task NotifyStaffAsync(
|
||||
string type,
|
||||
IReadOnlyDictionary<string, string?> payload,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await _client.RawQuery(
|
||||
"SELECT type::string(id) AS id FROM user WHERE role IN $roles;",
|
||||
new Dictionary<string, object?> { ["roles"] = StaffRoles },
|
||||
cancellationToken);
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<UserRow>>(0) ?? [];
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
await NotifyUserAsync(row.Id, type, payload, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task NotifyUserAsync(
|
||||
string userId,
|
||||
string type,
|
||||
IReadOnlyDictionary<string, string?> payload,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var notificationId = Guid.NewGuid().ToString("N");
|
||||
var (table, id) = SurrealRecordId.Split(userId);
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
"CREATE type::record('notification', $id) SET recipient = type::record($table, $recipientId), " +
|
||||
"type = $type, payload = $payload;",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["id"] = notificationId,
|
||||
["table"] = table,
|
||||
["recipientId"] = id,
|
||||
["type"] = type,
|
||||
["payload"] = payload,
|
||||
},
|
||||
cancellationToken);
|
||||
response.EnsureAllOks();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NotificationResponse>> ListForUserAsync(
|
||||
string userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (table, id) = SurrealRecordId.Split(userId);
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
$"SELECT {SelectFields} FROM notification WHERE recipient = type::record($table, $id) " +
|
||||
"ORDER BY created_at DESC;",
|
||||
new Dictionary<string, object?> { ["table"] = table, ["id"] = id },
|
||||
cancellationToken);
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<NotificationRow>>(0) ?? [];
|
||||
|
||||
return rows.Select(Map).ToList();
|
||||
}
|
||||
|
||||
public async Task<NotificationResponse?> MarkReadAsync(
|
||||
string notificationId,
|
||||
string userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (table, id) = SurrealRecordId.Split(notificationId);
|
||||
var (userTable, userIdPart) = SurrealRecordId.Split(userId);
|
||||
|
||||
var response = await _client.RawQuery(
|
||||
"UPDATE type::record($table, $id) SET read_at = time::now() " +
|
||||
"WHERE recipient = type::record($userTable, $userId); " +
|
||||
$"SELECT {SelectFields} FROM type::record($table, $id) LIMIT 1;",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["table"] = table,
|
||||
["id"] = id,
|
||||
["userTable"] = userTable,
|
||||
["userId"] = userIdPart,
|
||||
},
|
||||
cancellationToken);
|
||||
response.EnsureAllOks();
|
||||
|
||||
var rows = response.GetValue<List<NotificationRow>>(1) ?? [];
|
||||
|
||||
return rows.Count > 0 ? Map(rows[0]) : null;
|
||||
}
|
||||
|
||||
private static NotificationResponse Map(NotificationRow row) => new(
|
||||
row.Id,
|
||||
row.Type,
|
||||
row.Payload,
|
||||
row.ReadAt,
|
||||
row.CreatedAt);
|
||||
}
|
||||
Reference in New Issue
Block a user