Enhance UI, add AI integration, improve logging and database stats
This commit is contained in:
@@ -43,39 +43,39 @@ public class PolymarketApiClient
|
||||
public async Task<List<PolymarketTradeResponse>> GetTradesAsync(string walletAddress, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/activity?user={walletAddress}&limit={limit}";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, ct) ?? [];
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, "Data", ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<PolymarketTradeResponse>> GetMarketTradesAsync(string conditionId, int limit = 1000, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/trades?condition_id={conditionId}&limit={limit}";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, ct) ?? [];
|
||||
return await ExecuteWithRetryAsync<List<PolymarketTradeResponse>>(_client, url, "Data", ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<List<PolymarketPositionResponse>> GetPositionsAsync(string walletAddress, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/positions?user={walletAddress}&sizeThreshold=0.1&sortBy=CURRENT&sortOrder=DESC";
|
||||
return await ExecuteWithRetryAsync<List<PolymarketPositionResponse>>(_client, url, ct) ?? [];
|
||||
return await ExecuteWithRetryAsync<List<PolymarketPositionResponse>>(_client, url, "Data", ct) ?? [];
|
||||
}
|
||||
|
||||
public async Task<GammaMarketResponse?> GetMarketAsync(string conditionId, CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/markets?condition_id={conditionId}";
|
||||
var results = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, ct);
|
||||
var results = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, "Gamma", ct);
|
||||
return results?.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetch a batch of markets from the Gamma API with pagination.
|
||||
/// Fetch a batch of events (and their nested markets) from the Gamma API with pagination.
|
||||
/// Supports offset-based pagination via the offset parameter.
|
||||
/// </summary>
|
||||
public async Task<List<GammaMarketResponse>> GetMarketsAsync(int limit = 1000, int offset = 0, bool includeClosed = false, CancellationToken ct = default)
|
||||
public async Task<List<GammaEventResponse>> GetEventsAsync(int limit = 100, int offset = 0, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
var activeOnly = !includeClosed;
|
||||
var url = $"/markets?limit={limit}&offset={offset}&active={activeOnly.ToString().ToLower()}&closed={includeClosed.ToString().ToLower()}";
|
||||
_logger.LogDebug("Fetching markets: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<GammaMarketResponse>>(_gammaClient, url, ct);
|
||||
_logger.LogInformation("Fetched {Count} markets (offset={Offset}, closed={Closed})", result?.Count ?? 0, offset, includeClosed);
|
||||
var url = $"/events?limit={limit}&offset={offset}&active={activeOnly.ToString().ToLower()}&closed={includeClosed.ToString().ToLower()}";
|
||||
_logger.LogDebug("Fetching events: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<GammaEventResponse>>(_gammaClient, url, "Gamma", ct);
|
||||
_logger.LogInformation("Fetched {Count} events (offset={Offset}, closed={Closed})", result?.Count ?? 0, offset, includeClosed);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public class PolymarketApiClient
|
||||
{
|
||||
var url = $"/holders?market={conditionId}&limit={limit}";
|
||||
_logger.LogDebug("Fetching holders: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<HoldersResponse>>(_client, url, ct);
|
||||
var result = await ExecuteWithRetryAsync<List<HoldersResponse>>(_client, url, "Data", ct);
|
||||
_logger.LogInformation("Fetched holders for {Market}: {Count} token groups",
|
||||
conditionId.Length > 12 ? conditionId[..12] + "..." : conditionId, result?.Count ?? 0);
|
||||
return result ?? [];
|
||||
@@ -106,13 +106,15 @@ public class PolymarketApiClient
|
||||
{
|
||||
var url = $"/v1/leaderboard?limit={Math.Min(limit, 50)}&time_period={timePeriod}&order_by={orderBy}&category={category}";
|
||||
_logger.LogDebug("Fetching leaderboard: {Url}", url);
|
||||
var result = await ExecuteWithRetryAsync<List<LeaderboardEntry>>(_client, url, ct);
|
||||
var result = await ExecuteWithRetryAsync<List<LeaderboardEntry>>(_client, url, "Data", ct);
|
||||
_logger.LogInformation("Leaderboard returned {Count} entries", result?.Count ?? 0);
|
||||
return result ?? [];
|
||||
}
|
||||
|
||||
private async Task<T?> ExecuteWithRetryAsync<T>(HttpClient client, string url, CancellationToken ct, int attempt = 1)
|
||||
private async Task<T?> ExecuteWithRetryAsync<T>(HttpClient client, string url, string endpointGroup, CancellationToken ct, int attempt = 1)
|
||||
{
|
||||
await _rateLimiter.WaitAsync(PlatformType.Polymarket, ct, endpointGroup);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.GetAsync(url, ct);
|
||||
@@ -135,15 +137,14 @@ public class PolymarketApiClient
|
||||
waitTime = TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
_logger.LogWarning("⚠️ Rate limit exceeded (429) for Polymarket. Pausing for {WaitTime}s...", (int)waitTime.TotalSeconds);
|
||||
_logger.LogWarning("⚠️ Rate limit exceeded (429) for Polymarket {Group}. Pausing for {WaitTime}s...", endpointGroup, (int)waitTime.TotalSeconds);
|
||||
|
||||
_rateLimiter.ReportRateLimitExceeded(PlatformType.Polymarket, waitTime);
|
||||
_rateLimiter.ReportRateLimitExceeded(PlatformType.Polymarket, waitTime, endpointGroup);
|
||||
|
||||
if (attempt < 3)
|
||||
{
|
||||
await _rateLimiter.WaitAsync(PlatformType.Polymarket, ct);
|
||||
_logger.LogWarning("Retrying {Url} (attempt {NextAttempt})...", url, attempt + 1);
|
||||
return await ExecuteWithRetryAsync<T>(client, url, ct, attempt + 1);
|
||||
return await ExecuteWithRetryAsync<T>(client, url, endpointGroup, ct, attempt + 1);
|
||||
}
|
||||
|
||||
return default;
|
||||
@@ -172,7 +173,7 @@ public class PolymarketApiClient
|
||||
public async Task<List<PriceHistoryEntry>> GetPricesHistoryAsync(string clobTokenId, string interval = "6h", CancellationToken ct = default)
|
||||
{
|
||||
var url = $"/prices-history?market={clobTokenId}&interval={interval}";
|
||||
var result = await ExecuteWithRetryAsync<PolymarketPriceHistoryResponse>(_clobClient, url, ct);
|
||||
var result = await ExecuteWithRetryAsync<PolymarketPriceHistoryResponse>(_clobClient, url, "Clob", ct);
|
||||
return result?.History ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ public class GammaMarketResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("conditionId")] public string ConditionId { get; set; } = "";
|
||||
[JsonPropertyName("questionID")] public string QuestionId { get; set; } = "";
|
||||
[JsonPropertyName("question")] public string Question { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
@@ -121,11 +122,16 @@ public class GammaMarketResponse
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Volume { get; set; }
|
||||
|
||||
[JsonPropertyName("volume24hr")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Volume24hr { get; set; }
|
||||
|
||||
[JsonPropertyName("liquidityNum")]
|
||||
[JsonConverter(typeof(FlexibleDoubleConverter))]
|
||||
public double Liquidity { get; set; }
|
||||
|
||||
[JsonPropertyName("endDateIso")] public string? EndDate { get; set; }
|
||||
[JsonPropertyName("endDateIso")] public string? EndDateIso { get; set; }
|
||||
[JsonPropertyName("endDate")] public string? EndDate { get; set; }
|
||||
[JsonPropertyName("startDate")] public string? StartDate { get; set; }
|
||||
[JsonPropertyName("createdAt")] public string? CreatedAt { get; set; }
|
||||
[JsonPropertyName("closed")] public bool Closed { get; set; }
|
||||
@@ -136,7 +142,7 @@ public class GammaMarketResponse
|
||||
/// <summary>JSON string of outcomes, e.g. "[\"Yes\", \"No\"]"</summary>
|
||||
[JsonPropertyName("outcomes")] public string? Outcomes { get; set; }
|
||||
|
||||
/// <summary>JSON string of outcome prices, e.g. "[\"0.55\", \"0.45\"]"</summary>
|
||||
/// <summary>JSON string of outcomePrices, e.g. "[\"0.55\", \"0.45\"]"</summary>
|
||||
[JsonPropertyName("outcomePrices")] public string? OutcomePrices { get; set; }
|
||||
|
||||
/// <summary>JSON string of CLOB token IDs, e.g. "[\"12345...\", \"67890...\"]"</summary>
|
||||
@@ -148,6 +154,22 @@ public class GammaEventResponse
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
[JsonPropertyName("title")] public string Title { get; set; } = "";
|
||||
[JsonPropertyName("description")] public string? Description { get; set; }
|
||||
[JsonPropertyName("image")] public string? Image { get; set; }
|
||||
[JsonPropertyName("startDate")] public string? StartDate { get; set; }
|
||||
[JsonPropertyName("endDate")] public string? EndDate { get; set; }
|
||||
[JsonPropertyName("createdAt")] public string? CreatedAt { get; set; }
|
||||
[JsonPropertyName("active")] public bool Active { get; set; }
|
||||
[JsonPropertyName("closed")] public bool Closed { get; set; }
|
||||
[JsonPropertyName("tags")] public List<GammaTagResponse> Tags { get; set; } = [];
|
||||
[JsonPropertyName("markets")] public List<GammaMarketResponse> Markets { get; set; } = [];
|
||||
}
|
||||
|
||||
public class GammaTagResponse
|
||||
{
|
||||
[JsonPropertyName("id")] public string Id { get; set; } = "";
|
||||
[JsonPropertyName("label")] public string Label { get; set; } = "";
|
||||
[JsonPropertyName("slug")] public string Slug { get; set; } = "";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════
|
||||
|
||||
@@ -32,17 +32,19 @@ public class PolymarketProvider : IPlatformProvider
|
||||
|
||||
var mappedTrades = raw.Select(r =>
|
||||
{
|
||||
var wallet = r.User ?? r.ProxyWallet ?? "";
|
||||
var wallet = !string.IsNullOrEmpty(r.User) ? r.User :
|
||||
!string.IsNullOrEmpty(r.ProxyWallet) ? r.ProxyWallet :
|
||||
platformUserId;
|
||||
var side = MapTradeSide(r);
|
||||
var sideStr = side.ToString().ToUpperInvariant();
|
||||
// Compact format: {txHash}_{assetId}_{side} — no wallet in ID to reduce index size.
|
||||
// Wallet passed transiently via TransientWallet [NotMapped] for MarketHistoryWorker.
|
||||
// Format: {txHash}_{wallet}_{assetId}_{side}
|
||||
// Wallet must be included to avoid cross-user collisions in the global IX_Trades_Platform_PlatformTradeId index.
|
||||
return new Trade
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
||||
? $"{r.Timestamp}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash}_{r.Asset}_{sideStr}",
|
||||
? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
|
||||
MarketId = r.ConditionId ?? "",
|
||||
AssetId = r.Asset ?? "",
|
||||
Outcome = r.Outcome ?? "",
|
||||
@@ -51,13 +53,13 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Size = (decimal)r.Size,
|
||||
Amount = (decimal)(r.Price * r.Size),
|
||||
ExecutedAt = DateTimeOffset.FromUnixTimeSeconds(r.Timestamp).UtcDateTime,
|
||||
TransactionHash = r.TransactionHash,
|
||||
TransactionHash = r.TransactionHash?.ToLowerInvariant(),
|
||||
TraderId = 0,
|
||||
TransientWallet = wallet,
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return mappedTrades.GroupBy(t => t.PlatformTradeId).Select(g => g.First()).ToList();
|
||||
return mappedTrades.GroupBy(t => t.PlatformTradeId, StringComparer.OrdinalIgnoreCase).Select(g => g.First()).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -75,8 +77,8 @@ public class PolymarketProvider : IPlatformProvider
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformTradeId = string.IsNullOrEmpty(r.TransactionHash)
|
||||
? $"{r.Timestamp}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash}_{r.Asset}_{sideStr}",
|
||||
? $"{r.Timestamp}_{wallet}_{r.Asset}_{sideStr}"
|
||||
: $"{r.TransactionHash.ToLowerInvariant()}_{wallet}_{r.Asset}_{sideStr}",
|
||||
MarketId = r.ConditionId ?? "",
|
||||
AssetId = r.Asset ?? "",
|
||||
Outcome = r.Outcome ?? "",
|
||||
@@ -127,33 +129,76 @@ public class PolymarketProvider : IPlatformProvider
|
||||
public async Task<Market?> GetMarketAsync(string platformMarketId, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
_logger.LogDebug("Fetching market {MarketId}", platformMarketId);
|
||||
var raw = await _api.GetMarketAsync(platformMarketId, ct);
|
||||
if (raw == null)
|
||||
if (raw == null || string.IsNullOrEmpty(raw.ConditionId)) return null;
|
||||
|
||||
var parentTags = "";
|
||||
if (raw.Events != null && raw.Events.Count > 0)
|
||||
{
|
||||
_logger.LogWarning("Market {MarketId} not found", platformMarketId);
|
||||
return null;
|
||||
var ev = raw.Events[0];
|
||||
parentTags = ev.Tags != null ? string.Join(", ", ev.Tags.Select(t => t.Label)) : "";
|
||||
}
|
||||
var market = MapGammaMarket(raw, parentTags);
|
||||
|
||||
// Map the parent Event if available in the Market response
|
||||
if (raw.Events != null && raw.Events.Count > 0)
|
||||
{
|
||||
var rawEv = raw.Events[0];
|
||||
long.TryParse(rawEv.Id, out var numericEventId);
|
||||
market.Event = new Event
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformEventId = numericEventId,
|
||||
Slug = rawEv.Slug,
|
||||
Title = rawEv.Title,
|
||||
Description = rawEv.Description,
|
||||
ImageUrl = rawEv.Image,
|
||||
StartDate = DateTime.TryParse(rawEv.StartDate, out var esd) ? esd : null,
|
||||
EndDate = DateTime.TryParse(rawEv.EndDate, out var eed) ? eed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEv.CreatedAt, out var ecd) ? ecd : DateTime.UtcNow,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEv.Active,
|
||||
IsClosed = rawEv.Closed,
|
||||
Tags = rawEv.Tags != null && rawEv.Tags.Count > 0 ? string.Join(", ", rawEv.Tags.Select(t => t.Label)) : string.Empty,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback empty event if missing (should rarely happen for valid Polymarket markets)
|
||||
market.Event = new Event
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
Slug = "unknown-" + market.ConditionId,
|
||||
Title = "Unknown Event",
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation("Fetched market: {Question}", raw.Question);
|
||||
return MapGammaMarket(raw);
|
||||
return market;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Market>> GetMarketsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
public async Task<IReadOnlyList<Event>> GetEventsAsync(int limit = 100, string? cursor = null, bool includeClosed = false, CancellationToken ct = default)
|
||||
{
|
||||
using var _ = PlatformLogContext.Push(PlatformName);
|
||||
int offset = 0;
|
||||
if (!string.IsNullOrEmpty(cursor) && int.TryParse(cursor, out var parsed))
|
||||
offset = parsed;
|
||||
|
||||
_logger.LogInformation("Fetching markets batch (limit={Limit}, offset={Offset}, includeClosed={Closed})", limit, offset, includeClosed);
|
||||
var raw = await _api.GetMarketsAsync(limit, offset, includeClosed, ct);
|
||||
_logger.LogInformation("Fetched {Count} markets from Gamma API", raw.Count);
|
||||
_logger.LogInformation("Fetching events batch (limit={Limit}, offset={Offset}, includeClosed={Closed})", limit, offset, includeClosed);
|
||||
var rawEvents = await _api.GetEventsAsync(limit, offset, includeClosed, ct);
|
||||
_logger.LogInformation("Fetched {Count} events from Gamma API", rawEvents.Count);
|
||||
|
||||
return raw
|
||||
.Where(m => !string.IsNullOrEmpty(m.ConditionId) && !string.IsNullOrEmpty(m.ClobTokenIds))
|
||||
.Select(MapGammaMarket)
|
||||
.ToList();
|
||||
var events = new List<Event>();
|
||||
foreach (var rawEvent in rawEvents)
|
||||
{
|
||||
var ev = MapGammaEvent(rawEvent);
|
||||
events.Add(ev);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<DiscoveredTrader>> GetTopHoldersAsync(string platformMarketId, int limit = 20, CancellationToken ct = default)
|
||||
@@ -192,31 +237,67 @@ public class PolymarketProvider : IPlatformProvider
|
||||
|
||||
// ── Private helpers ──────────────────────────────────────────
|
||||
|
||||
private Market MapGammaMarket(GammaMarketResponse raw)
|
||||
private Event MapGammaEvent(GammaEventResponse rawEvent)
|
||||
{
|
||||
var eventSlug = "";
|
||||
if (raw.Events != null && raw.Events.Count > 0 && !string.IsNullOrEmpty(raw.Events[0].Slug))
|
||||
long.TryParse(rawEvent.Id, out var numericId);
|
||||
|
||||
var ev = new Event
|
||||
{
|
||||
eventSlug = raw.Events[0].Slug;
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformEventId = numericId,
|
||||
Slug = rawEvent.Slug,
|
||||
Title = rawEvent.Title,
|
||||
Description = rawEvent.Description,
|
||||
ImageUrl = rawEvent.Image,
|
||||
StartDate = DateTime.TryParse(rawEvent.StartDate, out var sd) ? sd : null,
|
||||
EndDate = DateTime.TryParse(rawEvent.EndDate, out var ed) ? ed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEvent.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEvent.Active,
|
||||
IsClosed = rawEvent.Closed,
|
||||
Tags = rawEvent.Tags != null && rawEvent.Tags.Count > 0
|
||||
? string.Join(", ", rawEvent.Tags.Select(t => t.Label))
|
||||
: string.Empty,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
if (rawEvent.Markets != null)
|
||||
{
|
||||
foreach (var rawMarket in rawEvent.Markets)
|
||||
{
|
||||
if (string.IsNullOrEmpty(rawMarket.ConditionId) || string.IsNullOrEmpty(rawMarket.ClobTokenIds)) continue;
|
||||
|
||||
var market = MapGammaMarket(rawMarket, ev.Tags ?? "");
|
||||
ev.Markets.Add(market);
|
||||
}
|
||||
}
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
private Market MapGammaMarket(GammaMarketResponse raw, string parentTags = "")
|
||||
{
|
||||
long.TryParse(raw.Id, out var marketNumericId);
|
||||
|
||||
var market = new Market
|
||||
{
|
||||
Platform = PlatformType.Polymarket,
|
||||
PlatformMarketId = raw.ConditionId,
|
||||
PlatformMarketId = marketNumericId,
|
||||
ConditionId = raw.ConditionId,
|
||||
QuestionId = raw.QuestionId,
|
||||
MarketSlug = raw.Slug,
|
||||
EventSlug = eventSlug,
|
||||
Description = raw.Description,
|
||||
ImageUrl = raw.Image,
|
||||
Question = raw.Question,
|
||||
Category = raw.Category,
|
||||
Category = string.IsNullOrWhiteSpace(raw.Category) ? parentTags : raw.Category,
|
||||
Volume = (decimal)raw.Volume,
|
||||
Volume24h = (decimal)raw.Volume24hr,
|
||||
Liquidity = (decimal)raw.Liquidity,
|
||||
StartDate = DateTime.TryParse(raw.StartDate, out var sd) ? sd : null,
|
||||
EndDate = DateTime.TryParse(raw.EndDate, out var ed) ? ed : null,
|
||||
CreatedAt = DateTime.TryParse(raw.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
|
||||
StartDate = DateTime.TryParse(raw.StartDate, out var msd) ? msd : null,
|
||||
EndDate = DateTime.TryParse(raw.EndDate ?? raw.EndDateIso, out var med) ? med : null,
|
||||
CreatedAt = DateTime.TryParse(raw.CreatedAt, out var mcd) ? mcd : DateTime.UtcNow,
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsResolved = raw.Resolved || raw.Closed, // Prefer resolved flag
|
||||
IsResolved = raw.Resolved || raw.Closed,
|
||||
ResolutionOutcome = raw.ResolutionOutcome,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user