Compare commits

...

23 Commits

Author SHA1 Message Date
b3cf03e3d4 fix: widen Rendimento/Rendimento Futuro columns to prevent mid-word wrap 2026-03-23 17:32:29 +01:00
088a95cafb fix: use Small/Header fonts in sottostanti table in expired renderer 2026-03-23 17:23:51 +01:00
f95242df1a fix: use Small/Header fonts in dividend table (consistent with eventi) 2026-03-23 17:23:50 +01:00
1f67285ae6 fix: use Small/Header fonts in sottostanti table (consistent with eventi) 2026-03-23 17:23:49 +01:00
b93a16ce6e fix: DrawSectionLabel title style + thin borders + correct font in dividend renderer 2026-03-23 17:17:49 +01:00
b1003bc3f1 fix: DrawSectionLabel title style in scenario renderer 2026-03-23 17:17:36 +01:00
031a1acc1a fix: DrawSectionLabel title style in eventi renderer 2026-03-23 17:17:21 +01:00
de7148c5cb fix: restore 'Soglia Rimborso' label in expired eventi (revert incorrect rename) 2026-03-23 17:07:53 +01:00
bd51bb4d26 fix: Italian labels + section title style + double header row height in dividend renderer 2026-03-23 17:05:39 +01:00
15a4034a77 fix: consistent title style in scenario renderer 2026-03-23 17:04:29 +01:00
9c78b3f852 fix: Italian column labels + consistent title style in eventi renderer
Changes applied:
1. Updated column names in active certificates (quotazione):
   - "Trigger Cedola" → "Livello Cedola"
   - "Trigger Autocall" → "Livello Richiamo Anticipato"
   - "Valore Autocall" → "Valore Richiamo Anticipato"

2. Updated column names in expired certificates:
   - "Soglia Rimborso" → "Livello Richiamo Anticipato"
   - "Valore Autocall" → "Valore Richiamo Anticipato"

3. Synchronized title style in EventiSectionRenderer with AnagraficaSectionRenderer:
   - Uses AccentBlue color instead of generic SectionTitle
   - Added blue separator line below title (AccentBluePen)
   - Aligned spacing and font sizing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:04:28 +01:00
3e52f807a5 fix: translate labels to Italian in anagrafica section (analisi + sottostanti) 2026-03-23 17:03:47 +01:00
1edfccfc62 fix: translate sottostanti column labels to Italian in expired anagrafica 2026-03-23 17:03:46 +01:00
26f818486c feat: wire DividendSectionRenderer into orchestrator — both active and expired flows
Also fix darkBlueBrush undefined reference in DividendSectionRenderer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:16:19 +01:00
e994352bd7 feat: register DividendSectionRenderer in DI container 2026-03-23 16:12:11 +01:00
069e33f0ec feat: add DividendSectionRenderer with landscape two-level header table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:09:41 +01:00
5b6400903d feat: skip sottostanti table in expired anagrafica when ShowDividend=true 2026-03-23 16:00:14 +01:00
35b2e9ae43 feat: skip sottostanti table in anagrafica when ShowDividend=true 2026-03-23 15:56:56 +01:00
02ca8bc9fb feat: add ?dividend query param to all report endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 15:53:53 +01:00
f79423219f feat: add showDividend param to IReportOrchestrator interface 2026-03-23 15:50:56 +01:00
2ddd5af043 feat: add ShowDividend flag to CertificateReportData 2026-03-23 15:49:10 +01:00
cc3d3695fe docs: fix spec review issues in dividend section design 2026-03-23 12:01:03 +01:00
001fbd90d9 docs: add dividend section design spec 2026-03-23 11:58:56 +01:00
11 changed files with 468 additions and 43 deletions

View File

@@ -43,7 +43,8 @@ public class ReportController : ControllerBase
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 = "branding")] bool showBranding = false,
[FromQuery(Name = "dividend")] bool showDividend = false)
{
string? isin = null;
@@ -70,7 +71,7 @@ public class ReportController : ControllerBase
return BadRequest("Specificare il parametro 'p' (ISIN cifrato) o 'alias' (alias ID).");
}
return await GenerateAndReturnPdf(isin, showBranding);
return await GenerateAndReturnPdf(isin, showBranding, showDividend);
}
/// <summary>
@@ -80,14 +81,15 @@ public class ReportController : ControllerBase
[HttpGet("by-isin/{isin}")]
public async Task<IActionResult> GenerateReportByIsin(
string isin,
[FromQuery(Name = "branding")] bool showBranding = false)
[FromQuery(Name = "branding")] bool showBranding = false,
[FromQuery(Name = "dividend")] bool showDividend = false)
{
if (string.IsNullOrWhiteSpace(isin) || isin.Length < 12)
{
return BadRequest("ISIN non valido.");
}
return await GenerateAndReturnPdf(isin, showBranding);
return await GenerateAndReturnPdf(isin, showBranding, showDividend);
}
/// <summary>
@@ -97,7 +99,8 @@ public class ReportController : ControllerBase
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 = "branding")] bool showBranding = false,
[FromQuery(Name = "dividend")] bool showDividend = false)
{
string? isin = null;
@@ -111,7 +114,7 @@ public class ReportController : ControllerBase
try
{
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding);
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding, showDividend);
return File(pdfBytes, "application/pdf", $"{isin}.pdf");
}
catch (Exception ex)
@@ -123,12 +126,12 @@ public class ReportController : ControllerBase
// ─── Helper ────────────────────────────────────────────────────────
private async Task<IActionResult> GenerateAndReturnPdf(string isin, bool showBranding)
private async Task<IActionResult> GenerateAndReturnPdf(string isin, bool showBranding = false, bool showDividend = false)
{
try
{
_logger.LogInformation("Richiesta report per ISIN {Isin}", isin);
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding);
var pdfBytes = await _orchestrator.GenerateReportAsync(isin, showBranding, showDividend);
// Inline: il PDF si apre direttamente nel browser
Response.Headers.Append("Content-Disposition", $"inline; filename={isin}.pdf");

View File

@@ -160,4 +160,5 @@ public class CertificateReportData
public ScenarioAnalysis Scenario { get; set; } = new();
public byte[]? ChartImage { get; set; }
public bool ShowBranding { get; set; } = false;
public bool ShowDividend { get; set; } = false;
}

View File

@@ -45,6 +45,7 @@ builder.Services.AddScoped<IPdfSectionRenderer, AnagraficaSectionRenderer>();
builder.Services.AddScoped<IPdfSectionRenderer, EventiSectionRenderer>();
builder.Services.AddScoped<IPdfSectionRenderer, ScenarioSectionRenderer>();
builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
builder.Services.AddScoped<DividendSectionRenderer>();
builder.Services.AddScoped<IChartSectionRenderer, ChartSectionRenderer>();
builder.Services.AddScoped<IPdfMergerService, PdfMergerService>();
builder.Services.AddScoped<IReportOrchestrator, ReportOrchestrator>();

View File

@@ -47,7 +47,7 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
y += SectionGap;
// ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
if (info.Sottostanti.Count > 0)
if (!data.ShowDividend && info.Sottostanti.Count > 0)
{
// Se lo spazio rimanente è meno di 80pt, nuova pagina
if (y > PageH - 80f)
@@ -330,14 +330,14 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
var rightItems = new (string Label, string Value)[]
{
("Rend. Capitale a Scadenza", info.CapitalReturnAtMaturity ?? "-"),
("IRR", info.IRR ?? "-"),
("Tasso Rendimento Interno", info.IRR ?? "-"),
("Protezione Capitale", info.BufferKProt ?? "-"),
("Protezione Coupon", info.BufferCPNProt ?? "-"),
("Valore Autocall", info.AutocallValue ?? "-"),
("Distanza Autocall", info.TriggerAutocallDistance ?? "-"),
("Rendimento Autocall", info.AutocallReturn ?? "-"),
("Protezione Cedola", info.BufferCPNProt ?? "-"),
("Valore Richiamo Anticipato", info.AutocallValue ?? "-"),
("Distanza Richiamo Anticipato", info.TriggerAutocallDistance ?? "-"),
("Rendimento Richiamo Anticipato", info.AutocallReturn ?? "-"),
("Fattore Airbag", string.IsNullOrWhiteSpace(info.FattoreAirbag) ? "—" : info.FattoreAirbag),
("Trigger OneStar", string.IsNullOrWhiteSpace(info.TriggerOneStar) ? "—" : info.TriggerOneStar),
("Livello OneStar", string.IsNullOrWhiteSpace(info.TriggerOneStar) ? "—" : info.TriggerOneStar),
};
float leftY = DrawKVList(g, leftItems, 0, ColW, y);
@@ -390,8 +390,8 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
// 9 colonne
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barriera Capitale",
"Buffer Capitale", "Trigger Cedola", "Buffer Cedola", "Trigger Autocall" };
string[] headers = { "Nome", "Livello Iniziale", "Ultimo Prezzo", "% Perf.", "Barriera Capitale",
"Protezione Capitale", "Livello Cedola", "Protezione Cedola", "Livello Richiamo Anticipato" };
foreach (var _ in headers) grid.Columns.Add();
@@ -400,7 +400,7 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
for (int i = 0; i < headers.Length; i++)
{
hr.Cells[i].Value = headers[i];
hr.Cells[i].Style.Font = PdfTheme.TableBold;
hr.Cells[i].Style.Font = PdfTheme.Header;
hr.Cells[i].Style.BackgroundBrush = PdfTheme.TableHeaderBrush;
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush;
hr.Cells[i].StringFormat = new PdfStringFormat(
@@ -425,7 +425,7 @@ public class AnagraficaSectionRenderer : IPdfSectionRenderer
for (int c = 0; c < headers.Length; c++)
{
row.Cells[c].Style.Font = PdfTheme.TableFont;
row.Cells[c].Style.Font = PdfTheme.Small;
row.Cells[c].StringFormat = new PdfStringFormat(
c == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right);
}

View File

@@ -0,0 +1,234 @@
using CertReports.Syncfusion.Helpers;
using CertReports.Syncfusion.Models;
using Syncfusion.Drawing;
using Syncfusion.Pdf;
using Syncfusion.Pdf.Graphics;
using Syncfusion.Pdf.Grid;
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 =
[
82f, // 0 Nome
50f, // 1 Livello Iniziale
50f, // 2 Ultimo Prezzo
40f, // 3 % Perf.
50f, // 4 Barriera Capitale
46f, // 5 Protezione Capitale
50f, // 6 Livello Cedola
46f, // 7 Protezione Cedola
46f, // 8 Livello Richiamo Anticipato
50f, // 9 Data Stacco
50f, // 10 Data Pagamento
40f, // 11 Importo
54f, // 12 Rendimento (+14)
40f, // 13 Importo Futuro
54f, // 14 Rendimento Futuro (+14)
];
private static readonly string[] Col2Headers =
[
"", // Nome (nessuna sub-label)
"Livello Iniziale", // Strike
"Ultimo Prezzo", // Last
"% Perf.", // % Perf. (invariato)
"Barriera Capitale", // Barr.Cap.
"Protezione Capitale", // Buf.Cap.
"Livello Cedola", // Trig.CPN
"Protezione Cedola", // Buf.CPN
"Livello Richiamo Anticipato", // Trig.AC
"Data Stacco", // (invariato)
"Data Pagamento", // Data Pag.
"Importo", // (invariato)
"Rendimento", // Rend.
"Importo Futuro", // Imp.Fut.
"Rendimento Futuro", // Rend.Fut.
];
// Indici colonne soggette a colore performance (0-based)
private static readonly HashSet<int> PerfCols = [3, 5, 7, 12, 14];
// Static brush e pen per header
private static readonly PdfSolidBrush DarkBlueBrush = new(Color.FromArgb(255, 10, 56, 128)); // #0A3880
private static readonly PdfPen SeparatorPen = new(Color.FromArgb(255, 100, 181, 246), 1.5f); // #64B5F6
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 pageWidth = size.Width;
float pageHeight = size.Height;
float w = pageWidth - 2 * PdfTheme.PageMargin;
float h = pageHeight - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;
float x0 = PdfTheme.PageMargin;
float y = PdfTheme.PageMargin;
// ── Titolo stile DrawSectionLabel ─────────────────────────────
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(x0, y, 3f, 14f));
g.DrawString("Sottostanti e Dividendi", PdfTheme.Bold,
new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(x0 + 6f, y, w - 6f, 14f));
y += 16f;
// ── Calcolo fattore scala 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 * 3;
// ── Tabella dati ──────────────────────────────────────────────
DrawDataGrid(g, data, x0, y, cw);
// ── Footer ────────────────────────────────────────────────────
PdfTheme.DrawFooter(g, pageWidth, pageHeight, 1, data.ShowBranding);
return doc;
}
private static void DrawHeader(PdfGraphics g, float x0, float y, float[] cw, float rh)
{
var headerFont = PdfTheme.Header;
var whiteBrush = PdfBrushes.White;
var accentBrush = PdfTheme.AccentBlueBrush;
// ── Riga 1: Gruppi ─────────────────────────────────────────────
float cx = x0;
// "Nome" (col 0) — cella vuota
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)
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 * 2, Col2Headers[i], bg, whiteBrush, headerFont);
cx += cw[i];
}
// ── Separatore verticale tra col 8 e col 9 ────────────────────
float sepX = x0 + cw.Take(9).Sum();
g.DrawLine(SeparatorPen, new PointF(sepX, y), new PointF(sepX, y + rh * 3));
}
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)
{
var grid = new PdfGrid();
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
grid.Style.Font = PdfTheme.Small;
// 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 = string.IsNullOrEmpty(s.Performance) ? "—" : s.Performance;
row.Cells[4].Value = string.IsNullOrEmpty(s.CapitalBarrier) ? "—" : s.CapitalBarrier;
row.Cells[5].Value = string.IsNullOrEmpty(s.ULCapitalBarrierBuffer) ? "—" : s.ULCapitalBarrierBuffer;
row.Cells[6].Value = string.IsNullOrEmpty(s.CouponBarrier) ? "—" : s.CouponBarrier;
row.Cells[7].Value = string.IsNullOrEmpty(s.ULCouponBarrierBuffer) ? "—" : s.ULCouponBarrierBuffer;
row.Cells[8].Value = string.IsNullOrEmpty(s.TriggerAutocall) ? "—" : s.TriggerAutocall;
row.Cells[9].Value = string.IsNullOrEmpty(s.DividendExDate) ? "—" : s.DividendExDate;
row.Cells[10].Value = string.IsNullOrEmpty(s.DividendPayDate) ? "—" : s.DividendPayDate;
row.Cells[11].Value = string.IsNullOrEmpty(s.DividendAmount) ? "—" : s.DividendAmount;
row.Cells[12].Value = string.IsNullOrEmpty(s.DividendYield) ? "—" : s.DividendYield;
row.Cells[13].Value = string.IsNullOrEmpty(s.DividendFutAmount) ? "—" : s.DividendFutAmount;
row.Cells[14].Value = string.IsNullOrEmpty(s.DividendFutYield) ? "—" : 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;
bool isNegative = val.TrimStart().StartsWith('-');
row.Cells[c].Style.TextBrush = isNegative
? PdfTheme.NegativeRedBrush
: PdfTheme.PositiveGreenBrush;
}
// Allineamento: Nome a sinistra, tutto il resto centrato
row.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Left);
for (int c = 1; c < 15; c++)
{
row.Cells[c].StringFormat = new PdfStringFormat
{
Alignment = PdfTextAlignment.Center,
LineAlignment = PdfVerticalAlignment.Middle,
};
}
}
// Pagina singola: specifica garantisce max ~20 sottostanti per certificato
PdfTheme.ApplyThinBorders(grid);
grid.Draw(g, new PointF(x0, y));
}
}

View File

@@ -30,9 +30,12 @@ public class EventiSectionRenderer : IPdfSectionRenderer
float y = 0;
// ── Titolo ─────────────────────────────────────────────────────
g.DrawString("Lista Eventi", PdfTheme.SectionTitleFont,
new PdfSolidBrush(PdfTheme.SectionTitle), new RectangleF(0, y, w, 18));
y += 22;
// Accent line verticale sinistra (3pt wide, 14pt tall) + testo blue su sfondo bianco
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, 3f, 14f));
g.DrawString("Lista Eventi", PdfTheme.Bold,
new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(6f, y, w, 14f));
y += 16f;
// ── Griglia ────────────────────────────────────────────────────
var grid = new PdfGrid();
@@ -48,12 +51,12 @@ public class EventiSectionRenderer : IPdfSectionRenderer
if (isExpired)
{
// Certificati non in quotazione: senza Ex Date e Record,
// rinominati Trigger Cedola→Barriera Cedola e Trigger Autocall→Soglia Rimborso
// rinominati Trigger Cedola→Barriera Cedola e Trigger Autocall→Livello Richiamo Anticipato
headers = new[]
{
"Osservazione", "Pagamento",
"Barriera Cedola", "Cedola %", "Pagato", "Memoria",
"Importo Pagato", "Soglia Rimborso", "Valore Autocall"
"Importo Pagato", "Soglia Rimborso", "Valore Richiamo Anticipato"
};
cw = new float[] { 62, 58, 52, 46, 36, 46, 52, 58, 52 };
paidColIndex = 4;
@@ -63,8 +66,8 @@ public class EventiSectionRenderer : IPdfSectionRenderer
headers = new[]
{
"Osservazione", "Ex Date", "Record", "Pagamento",
"Trigger Cedola", "Cedola %", "Pagato", "Memoria",
"Importo Pagato", "Trigger Autocall", "Valore Autocall"
"Livello Cedola", "Cedola %", "Pagato", "Memoria",
"Importo Pagato", "Livello Richiamo Anticipato", "Valore Richiamo Anticipato"
};
cw = new float[] { 62, 52, 52, 58, 52, 46, 36, 46, 52, 58, 52 };
paidColIndex = 6;

View File

@@ -46,7 +46,7 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
y += SectionGap;
// ── SEZIONE C: SOTTOSTANTI ────────────────────────────────────
if (info.Sottostanti.Count > 0)
if (!data.ShowDividend && info.Sottostanti.Count > 0)
{
if (y > PageH - 80f)
{
@@ -229,8 +229,8 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
var grid = new PdfGrid();
grid.Style.CellPadding = new PdfPaddings(2, 2, 2, 2);
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barriera Capitale",
"Buffer Capitale", "Trigger Cedola", "Buffer Cedola", "Trigger Autocall" };
string[] headers = { "Nome", "Livello Iniziale", "Ultimo Prezzo", "% Perf.", "Barriera Capitale",
"Protezione Capitale", "Livello Cedola", "Protezione Cedola", "Livello Richiamo Anticipato" };
foreach (var _ in headers) grid.Columns.Add();
@@ -238,7 +238,7 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
for (int i = 0; i < headers.Length; i++)
{
hr.Cells[i].Value = headers[i];
hr.Cells[i].Style.Font = PdfTheme.TableBold;
hr.Cells[i].Style.Font = PdfTheme.Header;
hr.Cells[i].Style.BackgroundBrush = PdfTheme.TableHeaderBrush;
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush;
hr.Cells[i].StringFormat = new PdfStringFormat(
@@ -262,7 +262,7 @@ public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
for (int c = 0; c < headers.Length; c++)
{
row.Cells[c].Style.Font = PdfTheme.TableFont;
row.Cells[c].Style.Font = PdfTheme.Small;
row.Cells[c].StringFormat = new PdfStringFormat(
c == 0 ? PdfTextAlignment.Left : PdfTextAlignment.Right);
}

View File

@@ -22,6 +22,7 @@ public class ReportOrchestrator : IReportOrchestrator
private readonly IPdfCacheService _cache;
private readonly ILogger<ReportOrchestrator> _logger;
private readonly ExpiredAnagraficaSectionRenderer _expiredAnagraficaRenderer;
private readonly DividendSectionRenderer _dividendRenderer;
public ReportOrchestrator(
ICertificateDataService dataService,
@@ -30,7 +31,8 @@ public class ReportOrchestrator : IReportOrchestrator
IPdfMergerService merger,
IPdfCacheService cache,
ILogger<ReportOrchestrator> logger,
ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer)
ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer,
DividendSectionRenderer dividendRenderer)
{
_dataService = dataService;
_sectionRenderers = sectionRenderers;
@@ -39,13 +41,15 @@ public class ReportOrchestrator : IReportOrchestrator
_cache = cache;
_logger = logger;
_expiredAnagraficaRenderer = expiredAnagraficaRenderer;
_dividendRenderer = dividendRenderer;
}
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false)
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false, bool showDividend = false)
{
// ── Cache check ────────────────────────────────────────────────
var baseCacheKey = showBranding ? $"{isin}:branded" : isin;
var expiredCacheKey = showBranding ? $"{isin}:expired:branded" : $"{isin}:expired";
var dividendSuffix = showDividend ? ":dividend" : "";
var baseCacheKey = showBranding ? $"{isin}:branded{dividendSuffix}" : $"{isin}{dividendSuffix}";
var expiredCacheKey = showBranding ? $"{isin}:expired:branded{dividendSuffix}" : $"{isin}:expired{dividendSuffix}";
var cached = _cache.Get(baseCacheKey) ?? _cache.Get(expiredCacheKey);
if (cached != null)
@@ -63,6 +67,7 @@ public class ReportOrchestrator : IReportOrchestrator
Eventi = await _dataService.GetCertificateEventsAsync(isin),
Scenario = await _dataService.GetScenarioAnalysisAsync(isin),
ShowBranding = showBranding,
ShowDividend = showDividend,
};
// ── 2. Determina il tipo di report ────────────────────────────
@@ -95,6 +100,20 @@ public class ReportOrchestrator : IReportOrchestrator
throw;
}
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;
}
}
try
{
var eventiRenderer = _sectionRenderers.First(r => r.SectionName == "Eventi");
@@ -130,6 +149,21 @@ public class ReportOrchestrator : IReportOrchestrator
_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;
}
}
}
}

View File

@@ -43,13 +43,13 @@ public class ScenarioSectionRenderer : IPdfSectionRenderer
float w = page.GetClientSize().Width;
float y = 0;
// ── Titolo centrato ────────────────────────────────────────────
var titleFont = new PdfStandardFont(PdfFontFamily.Helvetica, 16f, PdfFontStyle.Bold);
g.DrawString("Analisi Scenario", titleFont,
new PdfSolidBrush(Color.FromArgb(255, 46, 80, 144)),
new RectangleF(0, y, w, 28),
new PdfStringFormat(PdfTextAlignment.Center));
y += 40;
// ── Titolo ────────────────────────────────────────────────────
// Accent line verticale sinistra (3pt wide, 14pt tall) + testo blue su sfondo bianco
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, 3f, 14f));
g.DrawString("Analisi Scenario", PdfTheme.Bold,
new PdfSolidBrush(PdfTheme.AccentBlue),
new RectangleF(6f, y, w, 14f));
y += 16f;
var scenario = data.Scenario;
if (scenario.Rows.Count == 0)

View File

@@ -46,5 +46,5 @@ public interface IPdfMergerService
/// </summary>
public interface IReportOrchestrator
{
Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false);
Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false, bool showDividend = false);
}

View File

@@ -0,0 +1,149 @@
# Dividend Section — Design Spec
**Data:** 2026-03-23
**Progetto:** SmartReports / CertReports.Syncfusion
**Stato:** Approvato
---
## Obiettivo
Aggiungere il supporto ai dati dividendi dei sottostanti nel report PDF, attivabile tramite il parametro query `?dividend=true`. Quando attivo, i dati dividendi (già presenti nella SP `rpt_Details_UL_ISIN`) vengono visualizzati in una pagina landscape dedicata con tabella unificata Sottostanti+Dividendi a header raggruppati.
---
## Parametro API
```
?dividend=true → attiva la pagina dividendi
?dividend=false → comportamento invariato (default)
```
Disponibile su tutti gli endpoint report esistenti (`/by-isin/{isin}`, `?p=`, `?alias=`, `/download`).
---
## Comportamento
### Con `dividend=false` (default)
Il report è identico a quello attuale. Nessuna modifica visibile.
### Con `dividend=true`
1. La tabella Sottostanti **non viene renderizzata** in Sezione 1 (né in `AnagraficaSectionRenderer` né in `ExpiredAnagraficaSectionRenderer`).
2. Dopo la Sezione 1 (Anagrafica) e prima della Sezione 2 (Eventi) viene inserita una **pagina landscape dedicata** con la tabella unificata Sottostanti+Dividendi.
3. Il resto del report (Eventi, Scenario, Grafico) rimane invariato.
---
## Struttura tabella landscape
Pagina A4 orizzontale (landscape), margini standard (`PdfTheme.PageMargin`).
Larghezza utile calcolata a runtime: `var size = page.GetClientSize(); float w = size.Width - 2 * PdfTheme.PageMargin; float h = size.Height - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;` — NON usare costanti hardcoded. Font: `PdfTheme.TableFont` (7pt).
### Header a 2 livelli (colspan)
| Riga 1 | Nome | SOTTOSTANTE (×3) | BARRIERE (×5) | DIVIDENDI (×6) |
|--------|------|------------------|---------------|----------------|
| Riga 2 | — | Strike, Last, % Perf. | Barr.Cap., Buf.Cap., Trig.CPN, Buf.CPN, Trig.AC | Data Stacco, Data Pag., Importo, Rend., Imp.Fut., Rend.Fut. |
- Gruppo **SOTTOSTANTE** + **BARRIERE**: sfondo `#1565C0` (AccentBlue)
- Gruppo **DIVIDENDI**: sfondo `#0A3880` (blu scuro) per differenziazione visiva
- Separatore verticale blu (`#64B5F6`) tra colonna T.AC e colonna Data Stacco
- Colori performance: negativi `NegativeRed`, positivi `PositiveGreen`
- Righe alternate: `TableAltRow`
### Larghezze colonne indicative (totale ~770pt)
| Colonna | Width (pt) |
|---------|-----------|
| Nome | 90 |
| Strike | 50 |
| Last | 50 |
| % Perf. | 46 |
| Barr.Cap. | 50 |
| Buf.Cap. | 46 |
| Trig.CPN | 50 |
| Buf.CPN | 46 |
| Trig.AC | 46 |
| Data Stacco | 54 |
| Data Pag. | 54 |
| Importo | 40 |
| Rend. | 40 |
| Imp.Fut. | 40 |
| Rend.Fut. | 40 |
| **Totale** | **742** |
Le larghezze vengono scalate proporzionalmente: `float scale = w / total;` dove `w` proviene da `page.GetClientSize().Width - 2 * PdfTheme.PageMargin` (calcolato a runtime, non hardcoded).
---
## Mapping dati
I campi dividendi sono già nel modello `Sottostante` (da SP `rpt_Details_UL_ISIN`):
| Colonna PDF | Campo modello |
|-------------|--------------|
| Data Stacco | `DividendExDate` |
| Data Pag. | `DividendPayDate` |
| Importo | `DividendAmount` |
| Rend. | `DividendYield` |
| Imp.Fut. | `DividendFutAmount` |
| Rend.Fut. | `DividendFutYield` |
Nessuna modifica al data service o alle stored procedure.
---
## Componenti da modificare / creare
### 1. `CertificateReportData` (Models/CertificateModels.cs)
Aggiungere proprietà:
```csharp
public bool ShowDividend { get; set; } = false;
```
### 2. `ReportController` (Controllers/ReportController.cs)
Leggere il parametro `dividend` da query string e impostare `data.ShowDividend`.
### 3. `PdfCacheService`
La chiave cache deve includere il flag dividend per evitare collisioni:
- `{isin}` — no branding, no dividend
- `{isin}:branded` — branding, no dividend
- `{isin}:dividend` — no branding, dividend
- `{isin}:branded:dividend` — branding + dividend
### 4. `AnagraficaSectionRenderer`
Condizione nel metodo `Render()`: se `data.ShowDividend == true`, saltare il blocco "SEZIONE C: SOTTOSTANTI".
### 5. `ExpiredAnagraficaSectionRenderer`
Stessa condizione: se `data.ShowDividend == true`, saltare il blocco "SEZIONE C: SOTTOSTANTI".
### 6. `DividendSectionRenderer` (nuovo file)
- Classe: `DividendSectionRenderer`
- **Non** implementa `IPdfSectionRenderer` (non partecipa al ciclo ordinato)
- Registrazione: `AddScoped<DividendSectionRenderer>()` in `Program.cs`
- Metodo: `PdfDocument Render(CertificateReportData data)`
- Pagina landscape A4, titolo "Sottostanti e Dividendi", tabella con header a 2 livelli
- Dimensioni pagina: ricavare da `page.GetClientSize()``w = size.Width - 2 * PageMargin`, `h = size.Height - 2 * PageMargin - FooterHeight`
- Footer: `PdfTheme.DrawFooter(g, w, h, 1, data.ShowBranding)` — il numero pagina `1` è relativo alla pagina interna del documento parziale, coerente con lo stesso pattern usato da tutti gli altri renderer (la numerazione assoluta è una limitazione nota del sistema di merge)
### 7. `ReportOrchestrator`
- Aggiungere `DividendSectionRenderer dividendRenderer` come parametro del costruttore, accanto a `ExpiredAnagraficaSectionRenderer expiredRenderer` già presente — stesso pattern di iniezione diretta
- In entrambi i flussi (attivo ed expired): se `data.ShowDividend == true`, chiamare `dividendRenderer.Render(data)` e inserire il `PdfDocument` risultante nella lista documenti da mergiare **subito dopo** il documento della Sezione 1 (Anagrafica), prima di tutti gli altri
---
## Note implementative
- **Header colspan su PdfGrid**: Syncfusion `PdfGrid` non supporta nativamente colspan negli header. Implementare i due livelli di header come righe manuali disegnate con `DrawRectangle` + `DrawString` (seguire il pattern di `AnagraficaSectionRenderer.DrawEmittenteTable` per le altezze delle righe header: `rh = PdfTheme.RowHeight`). Riga 1 (gruppi): altezza `rh`; riga 2 (sotto-colonne): altezza `rh`. La `PdfGrid` dati viene disegnata a `y` iniziale = `y + rh * 2`. Le larghezze delle singole colonne nella riga 2 devono sommarsi esattamente alle larghezze dei gruppi in riga 1 per allineamento corretto.
- **Colori negativi/positivi**: applicare `ColorPerformanceCell` su % Perf., Buf.Cap., Buf.CPN, Rend., Rend.Fut. — stessa logica di `AnagraficaSectionRenderer`.
- **Valori assenti**: celle dividendo vuote mostrano `"—"`.
- Il parametro `dividend` è indipendente da `branding` — entrambi possono essere attivi contemporaneamente.
---
## Non in scope
- Modifiche alle stored procedure o al data service
- Nuove colonne nel modello `Sottostante`
- Modifiche al grafico o agli eventi
- Paginazione automatica della tabella dividendi (assumiamo max ~20 sottostanti per certificato)