using System; using System.Collections.Generic; using System.Globalization; using System.Linq; namespace ytLive.Models; /// One Super Chat or Super Sticker event, as YouTube reports it. public sealed record SuperChatEvent( string Id, ChatEventKind Kind, long AmountMicros, string Currency, string? DisplayAmount, DateTime PublishedAt); /// /// TASK 3·20: what this session's rewards added up to — in memory only. /// Derived on demand from superChatEvents.list rather than persisted: YouTube is the system /// of record for money, and keeping a local ledger would contradict the published promise that no /// viewer, member or donor information is retained after the session ends. /// Totals are kept per currency and never summed across currencies — a $5 and a €5 are not /// $10, and a single blended number would be a lie. /// public sealed record SessionRewardSummary( int SuperChatCount, int SuperStickerCount, IReadOnlyDictionary MicrosByCurrency, DateTime? FirstEventAt, DateTime? LastEventAt) { public static readonly SessionRewardSummary Empty = new( 0, 0, new Dictionary(), null, null); public int TotalEvents => SuperChatCount + SuperStickerCount; public bool HasEvents => TotalEvents > 0; /// Filters to a session window and rolls the events up. Events outside /// [, ] are ignored, which is what makes a /// report "this session" rather than "this channel". public static SessionRewardSummary From( IEnumerable events, DateTime sinceUtc, DateTime? untilUtc = null) { var until = untilUtc ?? DateTime.MaxValue; var inWindow = events .Where(e => e.PublishedAt >= sinceUtc && e.PublishedAt <= until) .OrderBy(e => e.PublishedAt) .ToList(); if (inWindow.Count == 0) return Empty; var byCurrency = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var e in inWindow) { var currency = string.IsNullOrWhiteSpace(e.Currency) ? "?" : e.Currency.ToUpperInvariant(); byCurrency.TryGetValue(currency, out var total); byCurrency[currency] = total + Math.Max(0, e.AmountMicros); } return new SessionRewardSummary( inWindow.Count(e => e.Kind == ChatEventKind.SuperChat), inWindow.Count(e => e.Kind == ChatEventKind.SuperSticker), byCurrency, inWindow[0].PublishedAt, inWindow[^1].PublishedAt); } /// Money lines for display, e.g. "USD $12.34 · EUR €4.00". Empty when there is nothing to /// show — a creator who never monetized must never be shown a zero. public IReadOnlyList MoneyLines() => MicrosByCurrency .Where(pair => pair.Value > 0) .OrderByDescending(pair => pair.Value) .Select(pair => $"{pair.Key} {Format(pair.Key, pair.Value)}") .ToList(); private static string Format(string currency, long micros) { var amount = micros / 1_000_000m; var symbol = currency switch { "USD" => "$", "EUR" => "€", "GBP" => "£", "CAD" => "CA$", "AUD" => "A$", _ => string.Empty, }; return symbol.Length > 0 ? symbol + amount.ToString("0.00", CultureInfo.InvariantCulture) : amount.ToString("0.00", CultureInfo.InvariantCulture) + " " + currency; } }