< Summary

Information
Class: Services.TransactionUtilsServices.TransactionCommentDisplayFormatter
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/TransactionUtilsServices/TransactionCommentDisplayFormatter.cs
Line coverage
92%
Covered lines: 143
Uncovered lines: 12
Coverable lines: 155
Total lines: 247
Line coverage: 92.2%
Branch coverage
84%
Covered branches: 59
Total branches: 70
Branch coverage: 84.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Parse(...)100%11100%
ParseForOrderHistory(...)75%4494.73%
WithExpandable(...)100%11100%
ParseCore(...)100%66100%
LooksLikeHtml(...)66.66%66100%
ParseStructured(...)84.61%302681.25%
FlushSection()100%88100%
ComputeExpandable(...)100%44100%
TryParseMetaLine(...)75%4490%
IsSectionHeader(...)80%1010100%
SplitParagraphs(...)50%2285.71%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/TransactionUtilsServices/TransactionCommentDisplayFormatter.cs

#LineLine coverage
 1using Services.TransactionCommentAi;
 2
 3namespace Services.TransactionUtilsServices;
 4
 5public static class TransactionCommentDisplayFormatter
 6{
 17    private static readonly HashSet<string> MetaLabels = new(StringComparer.OrdinalIgnoreCase)
 18    {
 19        "Тип",
 110        "Когда",
 111        "Пользователь",
 112        "Заказ",
 113        "ID транзакции"
 114    };
 15
 116    private static readonly HashSet<string> HiddenMetaLabels = new(StringComparer.OrdinalIgnoreCase)
 117    {
 118        "ID транзакции"
 119    };
 20
 121    private static readonly HashSet<string> OrderHistoryRedundantMetaLabels = new(StringComparer.OrdinalIgnoreCase)
 122    {
 123        "Тип",
 124        "Когда",
 125        "Пользователь",
 126        "Заказ"
 127    };
 28
 29    public static TransactionCommentDisplayModel Parse(string? comment) =>
 730        WithExpandable(ParseCore(comment));
 31
 32    public static TransactionCommentDisplayModel ParseForOrderHistory(string? comment)
 33    {
 234        var parsed = ParseCore(comment);
 235        if (parsed.Kind != TransactionCommentDisplayKind.Structured)
 036            return WithExpandable(parsed);
 37
 238        var meta = parsed.Meta
 639            .Where(m => !OrderHistoryRedundantMetaLabels.Contains(m.Label))
 240            .ToList();
 41
 242        var sections = parsed.Sections
 243            .Select(s => string.Equals(s.Title, "Что произошло", StringComparison.OrdinalIgnoreCase)
 244                ? new TransactionCommentBodySection(null, s.Body, s.IsWarning)
 245                : s)
 246            .ToList();
 47
 248        return WithExpandable(new TransactionCommentDisplayModel
 249        {
 250            Kind = TransactionCommentDisplayKind.Structured,
 251            DisplayMode = TransactionCommentDisplayMode.OrderHistoryCard,
 252            Raw = parsed.Raw,
 253            Meta = meta,
 254            Sections = sections
 255        });
 56    }
 57
 58    private static TransactionCommentDisplayModel WithExpandable(TransactionCommentDisplayModel model) =>
 959        new()
 960        {
 961            Kind = model.Kind,
 962            DisplayMode = model.DisplayMode,
 963            Raw = model.Raw,
 964            Meta = model.Meta,
 965            Sections = model.Sections,
 966            IsExpandable = ComputeExpandable(model)
 967        };
 68
 69    private static TransactionCommentDisplayModel ParseCore(string? comment)
 70    {
 971        if (string.IsNullOrWhiteSpace(comment))
 172            return new TransactionCommentDisplayModel { Kind = TransactionCommentDisplayKind.Empty };
 73
 874        if (TransactionCommentAiConstants.IsGeneratingPlaceholder(comment))
 75        {
 176            return new TransactionCommentDisplayModel
 177            {
 178                Kind = TransactionCommentDisplayKind.Generating,
 179                Raw = comment.Trim()
 180            };
 81        }
 82
 783        var trimmed = comment.Trim();
 784        if (LooksLikeHtml(trimmed))
 85        {
 186            return new TransactionCommentDisplayModel
 187            {
 188                Kind = TransactionCommentDisplayKind.Html,
 189                Raw = trimmed
 190            };
 91        }
 92
 693        return ParseStructured(trimmed);
 94    }
 95
 96    private static bool LooksLikeHtml(string text) =>
 797        text.Contains('<', StringComparison.Ordinal)
 798        && (text.Contains("</", StringComparison.Ordinal)
 799            || text.Contains("/>", StringComparison.Ordinal)
 7100            || text.Contains("class=", StringComparison.OrdinalIgnoreCase));
 101
 102    private static TransactionCommentDisplayModel ParseStructured(string text)
 103    {
 6104        var lines = text.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n');
 6105        var meta = new List<TransactionCommentMetaItem>();
 6106        var sections = new List<TransactionCommentBodySection>();
 107
 6108        var index = 0;
 18109        while (index < lines.Length)
 110        {
 18111            var line = lines[index].Trim();
 18112            if (string.IsNullOrEmpty(line))
 113            {
 4114                index++;
 4115                break;
 116            }
 117
 14118            if (!TryParseMetaLine(line, out var label, out var value))
 119                break;
 120
 12121            if (!HiddenMetaLabels.Contains(label))
 12122                meta.Add(new TransactionCommentMetaItem(label, value));
 123
 12124            index++;
 125        }
 126
 6127        while (index < lines.Length && string.IsNullOrWhiteSpace(lines[index]))
 0128            index++;
 129
 6130        var bodyLines = new List<string>();
 6131        string? currentTitle = null;
 132
 133        void FlushSection()
 134        {
 10135            var body = string.Join('\n', bodyLines).Trim();
 10136            if (string.IsNullOrEmpty(body) && string.IsNullOrEmpty(currentTitle))
 4137                return;
 138
 6139            var isWarning = (!string.IsNullOrEmpty(currentTitle) && currentTitle.StartsWith('⚠'))
 6140                            || body.StartsWith('⚠');
 6141            sections.Add(new TransactionCommentBodySection(currentTitle, body, isWarning));
 6142            bodyLines.Clear();
 6143            currentTitle = null;
 6144        }
 145
 26146        for (; index < lines.Length; index++)
 147        {
 10148            var trimmedLine = lines[index].Trim();
 10149            if (string.IsNullOrEmpty(trimmedLine))
 150            {
 0151                if (bodyLines.Count > 0)
 0152                    bodyLines.Add(string.Empty);
 0153                continue;
 154            }
 155
 10156            if (IsSectionHeader(trimmedLine))
 157            {
 4158                FlushSection();
 4159                currentTitle = trimmedLine.TrimEnd(':').Trim();
 4160                continue;
 161            }
 162
 6163            bodyLines.Add(trimmedLine);
 164        }
 165
 6166        FlushSection();
 167
 8168        if (meta.Count > 0 || sections.Any(s => !string.IsNullOrWhiteSpace(s.Title)))
 169        {
 4170            return new TransactionCommentDisplayModel
 4171            {
 4172                Kind = TransactionCommentDisplayKind.Structured,
 4173                Raw = text,
 4174                Meta = meta,
 4175                Sections = sections
 4176            };
 177        }
 178
 2179        if (sections.Count > 0)
 180        {
 2181            return new TransactionCommentDisplayModel
 2182            {
 2183                Kind = TransactionCommentDisplayKind.Plain,
 2184                Raw = sections[0].Body
 2185            };
 186        }
 187
 0188        return new TransactionCommentDisplayModel
 0189        {
 0190            Kind = TransactionCommentDisplayKind.Plain,
 0191            Raw = text
 0192        };
 193    }
 194
 195    private static bool ComputeExpandable(TransactionCommentDisplayModel model)
 196    {
 9197        if (model.Kind is not (TransactionCommentDisplayKind.Structured or TransactionCommentDisplayKind.Plain))
 3198            return false;
 199
 10200        var bodyLength = model.Sections.Sum(s => s.Body.Length);
 6201        return Math.Max(model.Raw.Length, bodyLength) > 280;
 202    }
 203
 204    private static bool TryParseMetaLine(string line, out string label, out string value)
 205    {
 14206        label = string.Empty;
 14207        value = string.Empty;
 208
 14209        var colonIndex = line.IndexOf(':');
 14210        if (colonIndex <= 0)
 2211            return false;
 212
 12213        label = line[..colonIndex].Trim();
 12214        if (!MetaLabels.Contains(label))
 0215            return false;
 216
 12217        value = line[(colonIndex + 1)..].Trim();
 12218        return true;
 219    }
 220
 221    private static bool IsSectionHeader(string line)
 222    {
 10223        if (line.StartsWith('⚠'))
 1224            return true;
 225
 9226        if (!line.EndsWith(':'))
 6227            return false;
 228
 3229        var title = line.TrimEnd(':').Trim();
 3230        return title.Equals("Что произошло", StringComparison.OrdinalIgnoreCase)
 3231               || title.StartsWith("Отгрузка в Ozon", StringComparison.OrdinalIgnoreCase)
 3232               || title.StartsWith("1С", StringComparison.OrdinalIgnoreCase)
 3233               || title.StartsWith("Ozon", StringComparison.OrdinalIgnoreCase);
 234    }
 235
 236    public static IReadOnlyList<string> SplitParagraphs(string body)
 237    {
 1238        if (string.IsNullOrWhiteSpace(body))
 0239            return [];
 240
 1241        return body
 1242            .Split("\n\n", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
 2243            .Select(p => p.Replace("\n", " ", StringComparison.Ordinal).Trim())
 2244            .Where(p => !string.IsNullOrWhiteSpace(p))
 1245            .ToList();
 246    }
 247}