Files
SmartReports/docs/superpowers/plans/2026-03-23-dividend-section.md
2026-07-21 11:02:32 +02:00

26 KiB

Dividend Section Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Aggiungere il parametro ?dividend=true che sostituisce la tabella Sottostanti nelle sezioni Anagrafica con una pagina landscape dedicata Sottostanti+Dividendi.

Architecture: Il flag ShowDividend viene aggiunto a CertificateReportData e propagato attraverso controller → orchestratore → renderer. DividendSectionRenderer è un renderer diretto (non IPdfSectionRenderer) iniettato nell'orchestratore come già avviene per ExpiredAnagraficaSectionRenderer. Entrambi i flussi (attivo + expired) ricevono la pagina landscape subito dopo la Sezione 1. Le chiavi cache vengono estese a 4 combinazioni.

Tech Stack: ASP.NET Core 8, Syncfusion PDF Net.Core v33, SkiaSharp, Microsoft.Data.SqlClient


File Map

File Azione
Models/CertificateModels.cs Modify — aggiungere ShowDividend
Services/Interfaces/IServices.cs Modify — aggiungere showDividend a IReportOrchestrator
Controllers/ReportController.cs Modify — aggiungere param dividend a tutti e 3 gli endpoint
Services/Implementations/ReportOrchestrator.cs Modify — signature, cache keys, inject renderer, inserire sezione
Services/Implementations/AnagraficaSectionRenderer.cs Modify — skip SEZIONE C se ShowDividend=true
Services/Implementations/ExpiredAnagraficaSectionRenderer.cs Modify — skip SEZIONE C se ShowDividend=true
Services/Implementations/DividendSectionRenderer.cs Create — nuova pagina landscape con tabella a 2 livelli
Program.cs Modify — registrare DividendSectionRenderer

Task 1: Aggiungere ShowDividend al modello

Files:

  • Modify: Models/CertificateModels.cs

  • Step 1: Leggere il file corrente

    Aprire Models/CertificateModels.cs e individuare la classe CertificateReportData. Il file contiene già public bool ShowBranding { get; set; } = false;.

  • Step 2: Aggiungere la proprietà

    Subito dopo ShowBranding:

    public bool ShowBranding { get; set; } = false;
    public bool ShowDividend { get; set; } = false;
    
  • Step 3: Build

    dotnet build CertReports.Syncfusion
    

    Expected: Build succeeded, 0 errors.

  • Step 4: Commit

    git add CertReports.Syncfusion/Models/CertificateModels.cs
    git commit -m "feat: add ShowDividend flag to CertificateReportData"
    

Task 2: Aggiornare l'interfaccia IReportOrchestrator

Files:

  • Modify: Services/Interfaces/IServices.cs

  • Step 1: Leggere il file

    Trovare IReportOrchestrator. Attualmente:

    public interface IReportOrchestrator
    {
        Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false);
    }
    
  • Step 2: Aggiungere il parametro

    public interface IReportOrchestrator
    {
        Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false, bool showDividend = false);
    }
    
  • Step 3: Build

    dotnet build CertReports.Syncfusion
    

    Expected: errore di compilazione su ReportOrchestrator (firma non corrisponde) — normale, fix al Task 4.

  • Step 4: Commit

    git add CertReports.Syncfusion/Services/Interfaces/IServices.cs
    git commit -m "feat: add showDividend param to IReportOrchestrator interface"
    

Task 3: Aggiornare ReportController — parametro dividend

Files:

  • Modify: Controllers/ReportController.cs

  • Step 1: Leggere il file

    Ci sono 3 endpoint che chiamano _orchestrator.GenerateReportAsync(isin, showBranding):

    • GenerateReport (GET /api/report?p=...)
    • GenerateReportByIsin (GET /api/report/by-isin/{isin})
    • DownloadReport (GET /api/report/download?p=...)
  • Step 2: Modificare GenerateReport

    Aggiungere il parametro dividend e passarlo all'orchestratore:

    public async Task<IActionResult> GenerateReport(
        [FromQuery(Name = "p")] string? encryptedIsin = null,
        [FromQuery(Name = "alias")] string? aliasId = null,
        [FromQuery(Name = "branding")] bool showBranding = false,
        [FromQuery(Name = "dividend")] bool showDividend = false)
    

    E nella chiamata:

    return await GenerateAndReturnPdf(isin, showBranding, showDividend);
    
  • Step 3: Modificare GenerateReportByIsin

    public async Task<IActionResult> GenerateReportByIsin(
        string isin,
        [FromQuery(Name = "branding")] bool showBranding = false,
        [FromQuery(Name = "dividend")] bool showDividend = false)
    

    E nella chiamata:

    return await GenerateAndReturnPdf(isin, showBranding, showDividend);
    
  • Step 4: Modificare DownloadReport

    public async Task<IActionResult> DownloadReport(
        [FromQuery(Name = "p")] string? encryptedIsin = null,
        [FromQuery(Name = "alias")] string? aliasId = null,
        [FromQuery(Name = "branding")] bool showBranding = false,
        [FromQuery(Name = "dividend")] bool showDividend = false)
    

    Nella chiamata interna:

    var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding, showDividend);
    
  • Step 5: Aggiornare il metodo privato GenerateAndReturnPdf

    Trovare la firma di GenerateAndReturnPdf e aggiungere bool showDividend = false, poi passarla a GenerateReportAsync:

    private async Task<IActionResult> GenerateAndReturnPdf(string isin, bool showBranding = false, bool showDividend = false)
    {
        // ...
        var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding, showDividend);
        // ...
    }
    
  • Step 6: Build

    dotnet build CertReports.Syncfusion
    

    Expected: errore su ReportOrchestrator (non ancora aggiornato) — normale.

  • Step 7: Commit

    git add CertReports.Syncfusion/Controllers/ReportController.cs
    git commit -m "feat: add ?dividend query param to all report endpoints"
    

Task 4: Aggiornare AnagraficaSectionRenderer — skip SEZIONE C

Files:

  • Modify: Services/Implementations/AnagraficaSectionRenderer.cs

  • Step 1: Leggere il file

    Trovare il blocco SEZIONE C. Attualmente:

    // ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
    if (info.Sottostanti.Count > 0)
    {
        // Se lo spazio rimanente è meno di 80pt, nuova pagina
        if (y > PageH - 80f)
        {
            PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding);
            page = doc.Pages.Add();
            g = page.Graphics;
            y = 0f;
        }
        DrawSottostanti(g, info.Sottostanti, PageW, y);
    }
    
  • Step 2: Wrappare il blocco con la condizione

    // ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
    if (!data.ShowDividend && info.Sottostanti.Count > 0)
    {
        // Se lo spazio rimanente è meno di 80pt, nuova pagina
        if (y > PageH - 80f)
        {
            PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding);
            page = doc.Pages.Add();
            g = page.Graphics;
            y = 0f;
        }
        DrawSottostanti(g, info.Sottostanti, PageW, y);
    }
    
  • Step 3: Build

    dotnet build CertReports.Syncfusion
    

    Expected: stesso errore su ReportOrchestrator — normale.

  • Step 4: Commit

    git add CertReports.Syncfusion/Services/Implementations/AnagraficaSectionRenderer.cs
    git commit -m "feat: skip sottostanti table in anagrafica when ShowDividend=true"
    

Task 5: Aggiornare ExpiredAnagraficaSectionRenderer — skip SEZIONE C

Files:

  • Modify: Services/Implementations/ExpiredAnagraficaSectionRenderer.cs

  • Step 1: Leggere il file

    Trovare il blocco SEZIONE C (struttura identica ad AnagraficaSectionRenderer):

    // ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
    if (info.Sottostanti.Count > 0)
    {
        if (y > PageH - 80f)
        {
            PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding);
            page = doc.Pages.Add();
            g = page.Graphics;
            ...
        }
        DrawSottostanti(g, info.Sottostanti, PageW, y);
    }
    
  • Step 2: Stessa modifica del Task 4

    // ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
    if (!data.ShowDividend && info.Sottostanti.Count > 0)
    {
        if (y > PageH - 80f)
        {
            PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding);
            page = doc.Pages.Add();
            g = page.Graphics;
            ...
        }
        DrawSottostanti(g, info.Sottostanti, PageW, y);
    }
    
  • Step 3: Build

    dotnet build CertReports.Syncfusion
    
  • Step 4: Commit

    git add CertReports.Syncfusion/Services/Implementations/ExpiredAnagraficaSectionRenderer.cs
    git commit -m "feat: skip sottostanti table in expired anagrafica when ShowDividend=true"
    

Task 6: Creare DividendSectionRenderer

Files:

  • Create: Services/Implementations/DividendSectionRenderer.cs

  • Step 1: Creare il file con la struttura base

    using CertReports.Syncfusion.Models;
    using Syncfusion.Pdf;
    using Syncfusion.Pdf.Graphics;
    using Syncfusion.Pdf.Grid;
    using System.Drawing;
    
    namespace CertReports.Syncfusion.Services.Implementations;
    
    /// <summary>
    /// Genera la pagina landscape con la tabella unificata Sottostanti + Dividendi.
    /// Non implementa IPdfSectionRenderer — iniettato direttamente nell'orchestratore.
    /// </summary>
    public class DividendSectionRenderer
    {
        // Larghezze colonne indicative (totale 742pt) — scalate proporzionalmente a runtime
        private static readonly float[] ColWidths =
        [
            90f,  // Nome
            50f,  // Strike
            50f,  // Last
            46f,  // % Perf.
            50f,  // Barr.Cap.
            46f,  // Buf.Cap.
            50f,  // Trig.CPN
            46f,  // Buf.CPN
            46f,  // Trig.AC
            54f,  // Data Stacco
            54f,  // Data Pag.
            40f,  // Importo
            40f,  // Rend.
            40f,  // Imp.Fut.
            40f,  // Rend.Fut.
        ];
    
        private static readonly string[] Col2Headers =
        [
            "", "Strike", "Last", "% Perf.",
            "Barr.Cap.", "Buf.Cap.", "Trig.CPN", "Buf.CPN", "Trig.AC",
            "Data Stacco", "Data Pag.", "Importo", "Rend.", "Imp.Fut.", "Rend.Fut.",
        ];
    
        // Indici colonne soggette a colore performance (0-based)
        private static readonly HashSet<int> PerfCols = [3, 5, 7, 12, 14];
    
        public PdfDocument Render(CertificateReportData data)
        {
            var doc = new PdfDocument();
            doc.PageSettings.Orientation = PdfPageOrientation.Landscape;
            doc.PageSettings.Size = PdfPageSize.A4;
    
            var page = doc.Pages.Add();
            var g = page.Graphics;
            var size = page.GetClientSize();
    
            float w = size.Width - 2 * PdfTheme.PageMargin;
            float h = size.Height - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;
    
            float x0 = PdfTheme.PageMargin;
            float y = PdfTheme.PageMargin;
    
            // ── Titolo ────────────────────────────────────────────────────
            var titleFont = PdfTheme.TitleFont;
            g.DrawString("Sottostanti e Dividendi", titleFont, PdfTheme.AccentBlueBrush,
                new RectangleF(x0, y, w, 20f));
            y += 20f + 4f;
    
            // ── Linea separatrice ─────────────────────────────────────────
            g.DrawLine(PdfTheme.AccentBluePen, new PointF(x0, y), new PointF(x0 + w, y));
            y += 6f;
    
            // ── Calcolo scale fattore su larghezza reale ──────────────────
            float totalNominal = ColWidths.Sum();
            float scale = w / totalNominal;
            float[] cw = ColWidths.Select(c => c * scale).ToArray();
    
            // ── Disegno header a 2 livelli ────────────────────────────────
            float rh = PdfTheme.RowHeight;
    
            DrawHeader(g, x0, y, cw, rh);
    
            y += rh * 2;
    
            // ── Tabella dati ──────────────────────────────────────────────
            DrawDataGrid(g, data, x0, y, cw, w);
    
            // ── Footer ────────────────────────────────────────────────────
            PdfTheme.DrawFooter(g, w, h, 1, data.ShowBranding);
    
            return doc;
        }
    
        private static void DrawHeader(PdfGraphics g, float x0, float y, float[] cw, float rh)
        {
            var headerFont = PdfTheme.TableFont;
            var whiteBrush = PdfBrushes.White;
            var accentBrush = PdfTheme.AccentBlueBrush;
            var darkBlueBrush = new PdfSolidBrush(Color.FromArgb(255, 10, 56, 128)); // #0A3880
    
            // ── Riga 1: Gruppi ────────────────────────────────────────────
            // "Nome" (col 0)
            float cx = x0;
            DrawHeaderCell(g, cx, y, cw[0], rh, "", accentBrush, whiteBrush, headerFont);
            cx += cw[0];
    
            // "SOTTOSTANTE" (cols 1-3)
            float sottostanteW = cw[1] + cw[2] + cw[3];
            DrawHeaderCell(g, cx, y, sottostanteW, rh, "SOTTOSTANTE", accentBrush, whiteBrush, headerFont);
            cx += sottostanteW;
    
            // "BARRIERE" (cols 4-8)
            float barriereW = cw[4] + cw[5] + cw[6] + cw[7] + cw[8];
            DrawHeaderCell(g, cx, y, barriereW, rh, "BARRIERE", accentBrush, whiteBrush, headerFont);
            cx += barriereW;
    
            // "DIVIDENDI" (cols 9-14) — blu scuro
            float dividendiW = cw[9] + cw[10] + cw[11] + cw[12] + cw[13] + cw[14];
            DrawHeaderCell(g, cx, y, dividendiW, rh, "DIVIDENDI", darkBlueBrush, whiteBrush, headerFont);
    
            // ── Riga 2: Sottocolonne ──────────────────────────────────────
            cx = x0;
            for (int i = 0; i < cw.Length; i++)
            {
                var bg = i >= 9 ? darkBlueBrush : accentBrush;
                DrawHeaderCell(g, cx, y + rh, cw[i], rh, Col2Headers[i], bg, whiteBrush, headerFont);
                cx += cw[i];
            }
    
            // ── Separatore verticale tra col 8 e col 9 ───────────────────
            float sepX = x0 + cw.Take(9).Sum();
            var sepPen = new PdfPen(Color.FromArgb(255, 100, 181, 246), 1.5f); // #64B5F6
            g.DrawLine(sepPen, new PointF(sepX, y), new PointF(sepX, y + rh * 2));
        }
    
        private static void DrawHeaderCell(PdfGraphics g, float x, float y, float w, float h,
            string text, PdfBrush bg, PdfBrush fg, PdfFont font)
        {
            g.DrawRectangle(bg, new RectangleF(x, y, w, h));
            if (!string.IsNullOrEmpty(text))
            {
                var fmt = new PdfStringFormat
                {
                    Alignment = PdfTextAlignment.Center,
                    LineAlignment = PdfVerticalAlignment.Middle,
                };
                g.DrawString(text, font, fg, new RectangleF(x + 1, y, w - 2, h), fmt);
            }
        }
    
        private static void DrawDataGrid(PdfGraphics g, CertificateReportData data,
            float x0, float y, float[] cw, float pageW)
        {
            var grid = new PdfGrid();
            grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
            grid.Style.Font = PdfTheme.TableFont;
    
            // 15 colonne
            grid.Columns.Add(15);
            for (int i = 0; i < 15; i++)
                grid.Columns[i].Width = cw[i];
    
            var sottostanti = data.Info.Sottostanti;
    
            for (int i = 0; i < sottostanti.Count; i++)
            {
                var s = sottostanti[i];
                var row = grid.Rows.Add();
    
                row.Cells[0].Value = s.Nome;
                row.Cells[1].Value = s.Strike;
                row.Cells[2].Value = s.LastPrice;
                row.Cells[3].Value = s.Performance ?? "—";
                row.Cells[4].Value = s.CapitalBarrier ?? "—";
                row.Cells[5].Value = s.ULCapitalBarrierBuffer ?? "—";
                row.Cells[6].Value = s.CouponBarrier ?? "—";
                row.Cells[7].Value = s.ULCouponBarrierBuffer ?? "—";
                row.Cells[8].Value = s.TriggerAutocall ?? "—";
                row.Cells[9].Value  = s.DividendExDate    ?? "—";
                row.Cells[10].Value = s.DividendPayDate   ?? "—";
                row.Cells[11].Value = s.DividendAmount    ?? "—";
                row.Cells[12].Value = s.DividendYield     ?? "—";
                row.Cells[13].Value = s.DividendFutAmount ?? "—";
                row.Cells[14].Value = s.DividendFutYield  ?? "—";
    
                // Righe alternate
                if (i % 2 == 1)
                {
                    for (int c = 0; c < 15; c++)
                        row.Cells[c].Style.BackgroundBrush = PdfTheme.TableAltRowBrush;
                }
    
                // Colore performance (negativi rosso, positivi verde)
                foreach (int c in PerfCols)
                {
                    var val = row.Cells[c].Value?.ToString();
                    if (string.IsNullOrEmpty(val) || val == "—") continue;
    
                    // Le stringhe arrivano già formattate dalla SP, es. "-12,34%"
                    bool isNegative = val.TrimStart().StartsWith('-');
                    row.Cells[c].Style.TextBrush = isNegative
                        ? PdfTheme.NegativeRedBrush
                        : PdfTheme.PositiveGreenBrush;
                }
    
                // Allineamento centrato per tutte le celle tranne Nome
                for (int c = 1; c < 15; c++)
                {
                    row.Cells[c].StringFormat = new PdfStringFormat
                    {
                        Alignment = PdfTextAlignment.Center,
                        LineAlignment = PdfVerticalAlignment.Middle,
                    };
                }
            }
    
            grid.Draw(g, new PointF(x0, y));
        }
    }
    
  • Step 2: Build

    dotnet build CertReports.Syncfusion
    

    Expected: errore su ReportOrchestrator — normale.

  • Step 3: Commit

    git add CertReports.Syncfusion/Services/Implementations/DividendSectionRenderer.cs
    git commit -m "feat: add DividendSectionRenderer with landscape two-level header table"
    

Task 7: Registrare DividendSectionRenderer in Program.cs

Files:

  • Modify: Program.cs

  • Step 1: Leggere Program.cs

    Trovare il blocco delle registrazioni renderer. Attualmente c'è:

    builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
    
  • Step 2: Aggiungere la registrazione subito sotto

    builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
    builder.Services.AddScoped<DividendSectionRenderer>();
    
  • Step 3: Build

    dotnet build CertReports.Syncfusion
    

    Expected: solo errore su ReportOrchestrator — normale.

  • Step 4: Commit

    git add CertReports.Syncfusion/Program.cs
    git commit -m "feat: register DividendSectionRenderer in DI container"
    

Task 8: Aggiornare ReportOrchestrator

Files:

  • Modify: Services/Implementations/ReportOrchestrator.cs

  • Step 1: Leggere il file completo

    Leggere l'intero file per avere contesto preciso prima di modificare.

  • Step 2: Aggiungere il campo e il parametro costruttore

    Aggiungere dopo _expiredAnagraficaRenderer:

    private readonly ExpiredAnagraficaSectionRenderer _expiredAnagraficaRenderer;
    private readonly DividendSectionRenderer _dividendRenderer;
    

    Nel costruttore aggiungere il parametro dopo expiredAnagraficaRenderer:

    ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer,
    DividendSectionRenderer dividendRenderer)
    

    E l'assegnazione:

    _expiredAnagraficaRenderer = expiredAnagraficaRenderer;
    _dividendRenderer = dividendRenderer;
    
  • Step 3: Aggiornare la signature del metodo e le chiavi cache

    Cambiare la firma:

    public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false, bool showDividend = false)
    

    Aggiornare le chiavi cache (le 4 combinazioni):

    var dividendSuffix = showDividend ? ":dividend" : "";
    
    var baseCacheKey    = showBranding ? $"{isin}:branded{dividendSuffix}"         : $"{isin}{dividendSuffix}";
    var expiredCacheKey = showBranding ? $"{isin}:expired:branded{dividendSuffix}" : $"{isin}:expired{dividendSuffix}";
    

    Nota: questo produce le 4 chiavi:

    • {isin} — no branding, no dividend
    • {isin}:branded — branding, no dividend
    • {isin}:dividend — no branding, dividend
    • {isin}:branded:dividend — branding + dividend
    • (+ varianti expired)
  • Step 4: Impostare ShowDividend nel CertificateReportData

    Nel blocco di inizializzazione:

    var reportData = new CertificateReportData
    {
        Info = await _dataService.GetCertificateInfoAsync(isin),
        Eventi = await _dataService.GetCertificateEventsAsync(isin),
        Scenario = await _dataService.GetScenarioAnalysisAsync(isin),
        ShowBranding = showBranding,
        ShowDividend = showDividend,
    };
    
  • Step 5: Inserire la sezione dividend nel flusso expired

    Dopo pdfSections.Add(_expiredAnagraficaRenderer.Render(reportData));, aggiungere:

    pdfSections.Add(_expiredAnagraficaRenderer.Render(reportData));
    
    if (reportData.ShowDividend)
    {
        try
        {
            pdfSections.Add(_dividendRenderer.Render(reportData));
            _logger.LogInformation("Sezione 'Dividend' generata per {Isin}", isin);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Errore nella sezione 'Dividend' per {Isin}", isin);
            throw;
        }
    }
    
  • Step 6: Inserire la sezione dividend nel flusso attivo (attuale)

    Il flusso attivo usa un foreach su _sectionRenderers.OrderBy(r => r.Order). AnagraficaSectionRenderer ha Order = 1 (Sezione 1). Dobbiamo inserire la pagina dividend subito dopo Anagrafica.

    Sostituire il foreach con:

    foreach (var renderer in _sectionRenderers.OrderBy(r => r.Order))
    {
        if (renderer.SectionName == "Scenario" && !isScenarioAllowed)
        {
            _logger.LogInformation("Sezione Scenario saltata per {Isin}", isin);
            continue;
        }
    
        try
        {
            pdfSections.Add(renderer.Render(reportData));
            _logger.LogInformation("Sezione '{Section}' generata per {Isin}", renderer.SectionName, isin);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Errore nella sezione '{Section}' per {Isin}", renderer.SectionName, isin);
            throw;
        }
    
        // Inserire pagina dividend subito dopo Anagrafica (Sezione 1)
        if (renderer.SectionName == "Anagrafica" && reportData.ShowDividend)
        {
            try
            {
                pdfSections.Add(_dividendRenderer.Render(reportData));
                _logger.LogInformation("Sezione 'Dividend' generata per {Isin}", isin);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Errore nella sezione 'Dividend' per {Isin}", isin);
                throw;
            }
        }
    }
    
  • Step 7: Build

    dotnet build CertReports.Syncfusion
    

    Expected: Build succeeded, 0 errors.

  • Step 8: Commit

    git add CertReports.Syncfusion/Services/Implementations/ReportOrchestrator.cs
    git commit -m "feat: wire DividendSectionRenderer into orchestrator — both active and expired flows"
    

Task 9: Verifica manuale

Non esistono test automatici nel progetto. Verificare manualmente:

  • Step 1: Avviare l'applicazione

    dotnet run --project CertReports.Syncfusion
    
  • Step 2: Test baseline — comportamento invariato

    Aprire nel browser:

    https://localhost:{porta}/api/report/by-isin/{ISIN_VALIDO}
    

    Expected: report normale con tabella Sottostanti visibile in Sezione 1. Nessuna pagina landscape.

  • Step 3: Test con ?dividend=true

    https://localhost:{porta}/api/report/by-isin/{ISIN_VALIDO}?dividend=true
    

    Expected:

    • Sezione 1 (Anagrafica): nessuna tabella Sottostanti
    • Dopo Sezione 1: pagina landscape con titolo "Sottostanti e Dividendi"
    • Header a 2 livelli: "SOTTOSTANTE" (blu) | "BARRIERE" (blu) | "DIVIDENDI" (blu scuro)
    • Separatore verticale tra colonna Trig.AC e Data Stacco
    • Righe dati con colori performance su % Perf., Buf.Cap., Buf.CPN, Rend., Rend.Fut.
    • Colonne dividendo vuote mostrano "—"
    • Footer con numero pagina
  • Step 4: Test combinato ?dividend=true&branding=true

    Expected: come sopra + footer con "Powered by Smart Roots" su tutte le pagine.

  • Step 5: Test su certificato expired

    Usare un ISIN con Stato != "Quotazione". Verificare lo stesso comportamento nei flusso expired.

  • Step 6: Commit finale

    git add -A
    git commit -m "feat: dividend section complete — landscape page with two-level header"
    

Note implementative

Gotcha Syncfusion v33 da rispettare

Problema Soluzione applicata nel piano
Color(r,g,b) rimosso Color.FromArgb(255, r, g, b) usato ovunque
grid.Draw() restituisce void Usato senza assegnazione
RectangleF no named args new RectangleF(x, y, w, h) — no named parameters
Namespace conflict Aggiungere using Syncfusion.Pdf; se necessario

Dimensioni pagina landscape

A4 landscape in Syncfusion: Width ≈ 841pt, Height ≈ 595pt. Con PageMargin = 30pt (verificare in PdfTheme), la larghezza utile w ≈ 781pt. Le colonne vengono scalate proporzionalmente da 742pt a wscale ≈ 1.05.

SectionName == "Anagrafica"

Il valore esatto confermato in AnagraficaSectionRenderer.cs è "Anagrafica" — la stringa di confronto nel Task 8 Step 6 è corretta.