Compare commits
12 Commits
a1ec6157f3
...
2546990556
| Author | SHA1 | Date | |
|---|---|---|---|
| 2546990556 | |||
| 7916cbc93b | |||
| f3e0a8254a | |||
| 2e320e6511 | |||
| 8c35cb5127 | |||
| b60956db9a | |||
| 571df139e5 | |||
| 3cca41a77b | |||
| 7ad14c129c | |||
| 26ebd320e5 | |||
| cdbdfeede1 | |||
| 71d6a4c32d |
@@ -75,6 +75,10 @@ public class CertificateInfo
|
|||||||
public string TriggerOneStar { get; set; } = string.Empty;
|
public string TriggerOneStar { get; set; } = string.Empty;
|
||||||
public string Note { get; set; } = string.Empty;
|
public string Note { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Stato quotazione (da SP rpt_Master_CFT_ISIN)
|
||||||
|
// Valori: "Quotazione" | "Revocato" | "Scaduto" | "Rimborsato"
|
||||||
|
public string Stato { get; set; } = string.Empty;
|
||||||
|
|
||||||
// Sottostanti (da SP separata: rpt_Details_UL_ISIN)
|
// Sottostanti (da SP separata: rpt_Details_UL_ISIN)
|
||||||
public List<Sottostante> Sottostanti { get; set; } = new();
|
public List<Sottostante> Sottostanti { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ builder.Services.AddScoped<IChartDataService, ChartDataService>();
|
|||||||
builder.Services.AddScoped<IPdfSectionRenderer, AnagraficaSectionRenderer>();
|
builder.Services.AddScoped<IPdfSectionRenderer, AnagraficaSectionRenderer>();
|
||||||
builder.Services.AddScoped<IPdfSectionRenderer, EventiSectionRenderer>();
|
builder.Services.AddScoped<IPdfSectionRenderer, EventiSectionRenderer>();
|
||||||
builder.Services.AddScoped<IPdfSectionRenderer, ScenarioSectionRenderer>();
|
builder.Services.AddScoped<IPdfSectionRenderer, ScenarioSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
|
||||||
builder.Services.AddScoped<IChartSectionRenderer, ChartSectionRenderer>();
|
builder.Services.AddScoped<IChartSectionRenderer, ChartSectionRenderer>();
|
||||||
builder.Services.AddScoped<IPdfMergerService, PdfMergerService>();
|
builder.Services.AddScoped<IPdfMergerService, PdfMergerService>();
|
||||||
builder.Services.AddScoped<IReportOrchestrator, ReportOrchestrator>();
|
builder.Services.AddScoped<IReportOrchestrator, ReportOrchestrator>();
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ public class CertificateDataService : ICertificateDataService
|
|||||||
info.CpnDaPagare = r.GetNullableDecimal("CpnDaPagare");
|
info.CpnDaPagare = r.GetNullableDecimal("CpnDaPagare");
|
||||||
info.CpnPagati = r.GetNullableDecimal("CpnPagati");
|
info.CpnPagati = r.GetNullableDecimal("CpnPagati");
|
||||||
info.RendimentoAttuale = r.GetStringSafe("RendimentoAttuale");
|
info.RendimentoAttuale = r.GetStringSafe("RendimentoAttuale");
|
||||||
|
info.Stato = r.GetStringSafe("Stato");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,13 +38,39 @@ public class EventiSectionRenderer : IPdfSectionRenderer
|
|||||||
var grid = new PdfGrid();
|
var grid = new PdfGrid();
|
||||||
grid.Style.CellPadding = new PdfPaddings(3, 3, 2, 2);
|
grid.Style.CellPadding = new PdfPaddings(3, 3, 2, 2);
|
||||||
|
|
||||||
// Colonne mappate sui campi della SP rpt_Events_CFT_ISIN
|
bool isExpired = !string.IsNullOrEmpty(data.Info.Stato) && data.Info.Stato != "Quotazione";
|
||||||
string[] headers =
|
|
||||||
|
// Colonne e larghezze dipendono dal tipo di certificato
|
||||||
|
string[] headers;
|
||||||
|
float[] cw;
|
||||||
|
int paidColIndex;
|
||||||
|
|
||||||
|
if (isExpired)
|
||||||
{
|
{
|
||||||
"Osservazione", "Ex Date", "Record", "Pagamento",
|
// Certificati non in quotazione: senza Ex Date e Record,
|
||||||
"Trigger Cedola", "Cedola %", "Pagato", "Memoria",
|
// rinominati Trigger Cedola→Barriera Cedola e Trigger Autocall→Soglia Rimborso,
|
||||||
"Importo Pagato", "Trigger Autocall", "Valore Autocall"
|
// aggiunte Barriera Capitale e Rimborso Capitale
|
||||||
};
|
headers = new[]
|
||||||
|
{
|
||||||
|
"Osservazione", "Pagamento",
|
||||||
|
"Barriera Cedola", "Cedola %", "Pagato", "Memoria",
|
||||||
|
"Importo Pagato", "Soglia Rimborso", "Valore Autocall",
|
||||||
|
"Barriera Capitale", "Rimborso Capitale"
|
||||||
|
};
|
||||||
|
cw = new float[] { 62, 58, 52, 46, 36, 46, 52, 58, 52, 58, 58 };
|
||||||
|
paidColIndex = 4;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
headers = new[]
|
||||||
|
{
|
||||||
|
"Osservazione", "Ex Date", "Record", "Pagamento",
|
||||||
|
"Trigger Cedola", "Cedola %", "Pagato", "Memoria",
|
||||||
|
"Importo Pagato", "Trigger Autocall", "Valore Autocall"
|
||||||
|
};
|
||||||
|
cw = new float[] { 62, 52, 52, 58, 52, 46, 36, 46, 52, 58, 52 };
|
||||||
|
paidColIndex = 6;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var _ in headers) grid.Columns.Add();
|
foreach (var _ in headers) grid.Columns.Add();
|
||||||
|
|
||||||
@@ -65,19 +91,34 @@ public class EventiSectionRenderer : IPdfSectionRenderer
|
|||||||
var evt = data.Eventi[i];
|
var evt = data.Eventi[i];
|
||||||
var row = grid.Rows.Add();
|
var row = grid.Rows.Add();
|
||||||
|
|
||||||
row.Cells[0].Value = evt.ObservationDate;
|
if (isExpired)
|
||||||
row.Cells[1].Value = evt.ExDate;
|
{
|
||||||
row.Cells[2].Value = evt.RecordDate;
|
row.Cells[0].Value = evt.ObservationDate;
|
||||||
row.Cells[3].Value = evt.PaymentDate;
|
row.Cells[1].Value = evt.PaymentDate;
|
||||||
row.Cells[4].Value = evt.CouponTrigger;
|
row.Cells[2].Value = evt.CouponTrigger;
|
||||||
row.Cells[5].Value = evt.CouponValue;
|
row.Cells[3].Value = evt.CouponValue;
|
||||||
row.Cells[6].Value = evt.Paid;
|
row.Cells[4].Value = evt.Paid;
|
||||||
row.Cells[7].Value = evt.Memory;
|
row.Cells[5].Value = evt.Memory;
|
||||||
row.Cells[8].Value = evt.AmountPaid;
|
row.Cells[6].Value = evt.AmountPaid;
|
||||||
//row.Cells[9].Value = evt.CapitalTrigger;
|
row.Cells[7].Value = evt.AutocallTrigger;
|
||||||
//row.Cells[10].Value = evt.CapitalValue;
|
row.Cells[8].Value = evt.AutocallValue;
|
||||||
row.Cells[9].Value = evt.AutocallTrigger;
|
row.Cells[9].Value = evt.CapitalTrigger;
|
||||||
row.Cells[10].Value = evt.AutocallValue;
|
row.Cells[10].Value = evt.CapitalValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
row.Cells[0].Value = evt.ObservationDate;
|
||||||
|
row.Cells[1].Value = evt.ExDate;
|
||||||
|
row.Cells[2].Value = evt.RecordDate;
|
||||||
|
row.Cells[3].Value = evt.PaymentDate;
|
||||||
|
row.Cells[4].Value = evt.CouponTrigger;
|
||||||
|
row.Cells[5].Value = evt.CouponValue;
|
||||||
|
row.Cells[6].Value = evt.Paid;
|
||||||
|
row.Cells[7].Value = evt.Memory;
|
||||||
|
row.Cells[8].Value = evt.AmountPaid;
|
||||||
|
row.Cells[9].Value = evt.AutocallTrigger;
|
||||||
|
row.Cells[10].Value = evt.AutocallValue;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var cell in row.Cells.OfType<PdfGridCell>())
|
foreach (var cell in row.Cells.OfType<PdfGridCell>())
|
||||||
{
|
{
|
||||||
@@ -92,11 +133,10 @@ public class EventiSectionRenderer : IPdfSectionRenderer
|
|||||||
|
|
||||||
// Evidenzia "SI" nella colonna Pagato
|
// Evidenzia "SI" nella colonna Pagato
|
||||||
if (evt.Paid == "SI")
|
if (evt.Paid == "SI")
|
||||||
row.Cells[6].Style.TextBrush = PdfTheme.PositiveBrush as PdfBrush;
|
row.Cells[paidColIndex].Style.TextBrush = PdfTheme.PositiveBrush as PdfBrush;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Larghezze colonne (landscape A4 ~ 757 punti utili)
|
// Larghezze colonne (landscape A4 ~ 757 punti utili)
|
||||||
float[] cw = { 62, 52, 52, 58, 52, 46, 36, 46, 52, 58, 52 };
|
|
||||||
float total = cw.Sum();
|
float total = cw.Sum();
|
||||||
float scale = w / total;
|
float scale = w / total;
|
||||||
for (int i = 0; i < cw.Length; i++)
|
for (int i = 0; i < cw.Length; i++)
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sezione 1 — Prima pagina del report per certificati non in quotazione
|
||||||
|
/// (Scaduto, Rimborsato, Revocato). Struttura semplificata rispetto al report attivo:
|
||||||
|
/// solo Titolo + Caratteristiche Prodotto + Analisi. Niente Bid/Ask, niente Sottostanti.
|
||||||
|
/// </summary>
|
||||||
|
public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
|
||||||
|
{
|
||||||
|
public string SectionName => "ExpiredAnagrafica";
|
||||||
|
public int Order => 1;
|
||||||
|
|
||||||
|
private const float PageW = 595f - 2 * PdfTheme.PageMargin;
|
||||||
|
private const float PageH = 842f - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;
|
||||||
|
private const float ColGap = 10f;
|
||||||
|
private const float ColW = (PageW - ColGap) / 2f;
|
||||||
|
private const float SectionGap = 10f;
|
||||||
|
|
||||||
|
public PdfDocument Render(CertificateReportData data)
|
||||||
|
{
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
var page = PdfTheme.AddA4Page(doc);
|
||||||
|
var g = page.Graphics;
|
||||||
|
var info = data.Info;
|
||||||
|
float y = 0f;
|
||||||
|
|
||||||
|
// ── TITOLO ────────────────────────────────────────────────────
|
||||||
|
y = DrawTitle(g, info, PageW, y);
|
||||||
|
|
||||||
|
// ── SEZIONE A: CARATTERISTICHE PRODOTTO ───────────────────────
|
||||||
|
y = DrawSectionLabel(g, "Caratteristiche Prodotto", y);
|
||||||
|
y = DrawCaratteristiche(g, info, y);
|
||||||
|
y += SectionGap;
|
||||||
|
|
||||||
|
// ── SEZIONE B: ANALISI ────────────────────────────────────────
|
||||||
|
y = DrawSectionLabel(g, "Analisi", y);
|
||||||
|
y = DrawAnalisi(g, info, y);
|
||||||
|
|
||||||
|
PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// TITOLO — Solo Tipologia box (niente Data/Bid/Ask)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawTitle(PdfGraphics g, CertificateInfo info, float w, float y)
|
||||||
|
{
|
||||||
|
g.DrawString($"Scheda Prodotto {info.Isin}",
|
||||||
|
PdfTheme.SectionTitleFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.AccentBlue),
|
||||||
|
new RectangleF(0, y, w, 20f),
|
||||||
|
new PdfStringFormat(PdfTextAlignment.Center));
|
||||||
|
y += 24f;
|
||||||
|
|
||||||
|
g.DrawLine(PdfTheme.AccentBluePen, 0, y, w, y);
|
||||||
|
y += 8f;
|
||||||
|
|
||||||
|
// Solo box Tipologia, niente Data/Bid/Ask
|
||||||
|
const float boxH = 28f;
|
||||||
|
if (!string.IsNullOrEmpty(info.Categoria))
|
||||||
|
{
|
||||||
|
DrawInfoBox(g, 0, y, w, boxH,
|
||||||
|
"TIPOLOGIA", info.Categoria,
|
||||||
|
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
|
||||||
|
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
|
||||||
|
PdfTheme.Bold);
|
||||||
|
y += boxH + 10f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// INTESTAZIONE DI SEZIONE
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawSectionLabel(PdfGraphics g, string title, float y)
|
||||||
|
{
|
||||||
|
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, 3f, 14f));
|
||||||
|
g.DrawString(title, PdfTheme.Bold,
|
||||||
|
new PdfSolidBrush(PdfTheme.AccentBlue),
|
||||||
|
new RectangleF(6f, y, PageW, 14f));
|
||||||
|
return y + 16f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// INFO BOX
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void DrawInfoBox(
|
||||||
|
PdfGraphics g,
|
||||||
|
float x, float y, float w, float boxH,
|
||||||
|
string label, string value,
|
||||||
|
PdfBrush bgBrush, PdfBrush accentBrush,
|
||||||
|
PdfBrush labelBrush, PdfBrush valueBrush,
|
||||||
|
PdfFont valueFont)
|
||||||
|
{
|
||||||
|
g.DrawRectangle(bgBrush, new RectangleF(x, y, w, boxH));
|
||||||
|
g.DrawRectangle(accentBrush, new RectangleF(x, y, 4f, boxH));
|
||||||
|
float innerX = x + 4f + 6f;
|
||||||
|
float innerW = w - 4f - 6f - 4f;
|
||||||
|
g.DrawString(label, PdfTheme.Small, labelBrush,
|
||||||
|
new RectangleF(innerX, y + 4f, innerW, 10f));
|
||||||
|
g.DrawString(value, valueFont, valueBrush,
|
||||||
|
new RectangleF(innerX, y + 14f, innerW, 14f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// SEZIONE A: CARATTERISTICHE PRODOTTO
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawCaratteristiche(PdfGraphics g, CertificateInfo info, float y)
|
||||||
|
{
|
||||||
|
return DrawEmittenteTable(g, info, 0, ColW, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float DrawEmittenteTable(PdfGraphics g, CertificateInfo info, float x, float w, float y)
|
||||||
|
{
|
||||||
|
float rh = PdfTheme.RowHeight;
|
||||||
|
float pad = PdfTheme.CellPadding;
|
||||||
|
|
||||||
|
// Header blu
|
||||||
|
g.DrawRectangle(PdfTheme.TableHeaderBrush, new RectangleF(x, y, w, rh));
|
||||||
|
string headerText = string.IsNullOrEmpty(info.Emittente)
|
||||||
|
? "EMITTENTE"
|
||||||
|
: $"EMITTENTE {info.Emittente.ToUpper()}";
|
||||||
|
g.DrawString(headerText, PdfTheme.TableBold,
|
||||||
|
PdfTheme.HeaderTextBrush,
|
||||||
|
new RectangleF(x + pad, y + 3f, w - pad * 2, rh));
|
||||||
|
y += rh;
|
||||||
|
|
||||||
|
var rows = new[]
|
||||||
|
{
|
||||||
|
("ISIN", info.Isin),
|
||||||
|
("Mercato", info.Mercato),
|
||||||
|
("Valuta", info.Valuta),
|
||||||
|
("Data Emissione", info.DataEmissione),
|
||||||
|
("Valore Rimborso", info.ValoreRimborso),
|
||||||
|
("Data Rimborso", info.DataRimborso),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < rows.Length; i++)
|
||||||
|
{
|
||||||
|
var (label, value) = rows[i];
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) continue;
|
||||||
|
|
||||||
|
bool alt = i % 2 == 1;
|
||||||
|
if (alt)
|
||||||
|
g.DrawRectangle(new PdfSolidBrush(PdfTheme.TableAltRow),
|
||||||
|
new RectangleF(x, y, w, rh));
|
||||||
|
|
||||||
|
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
|
||||||
|
|
||||||
|
g.DrawString(label, PdfTheme.TableFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.TextSecondary),
|
||||||
|
new RectangleF(x + pad, y + 2f, w * 0.5f, rh));
|
||||||
|
|
||||||
|
var valueBrush = label == "ISIN"
|
||||||
|
? new PdfSolidBrush(PdfTheme.AccentBlue)
|
||||||
|
: new PdfSolidBrush(PdfTheme.TextPrimary);
|
||||||
|
|
||||||
|
g.DrawString(value, PdfTheme.TableBold, valueBrush,
|
||||||
|
new RectangleF(x + w * 0.5f, y + 2f, w * 0.5f - pad, rh),
|
||||||
|
new PdfStringFormat(PdfTextAlignment.Right));
|
||||||
|
|
||||||
|
y += rh;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
|
||||||
|
return y + 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// SEZIONE B: ANALISI — KV list allineata a sinistra, larghezza piena
|
||||||
|
// Le etichette sono sempre visibili; valore "-" se assente dal DB.
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawAnalisi(PdfGraphics g, CertificateInfo info, float y)
|
||||||
|
{
|
||||||
|
var items = new (string Label, string Value)[]
|
||||||
|
{
|
||||||
|
("Importo Cedola (p.a.)", info.NominalAnnualYield),
|
||||||
|
("Frequenza Cedola", info.FrequenzaCedole),
|
||||||
|
("Valore Nominale", info.NominalValue?.ToString("N0") ?? string.Empty),
|
||||||
|
("Memoria Cedola", info.Memory),
|
||||||
|
("Tipo Barriera", info.BarrierType),
|
||||||
|
("Tipo Basket", info.BasketType),
|
||||||
|
("Rendimento Totale", info.RendimentoTotale),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Posizione x=0, stessa larghezza della tabella Caratteristiche Prodotto
|
||||||
|
return DrawKVList(g, items, 0, ColW, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float DrawKVList(PdfGraphics g, (string Label, string Value)[] items, float x, float w, float y)
|
||||||
|
{
|
||||||
|
float rh = PdfTheme.RowHeight;
|
||||||
|
float pad = PdfTheme.CellPadding;
|
||||||
|
float labelW = w * 0.35f;
|
||||||
|
float valueW = w * 0.65f;
|
||||||
|
|
||||||
|
foreach (var (label, rawValue) in items)
|
||||||
|
{
|
||||||
|
// L'etichetta è sempre visibile; se il valore è assente mostro "-"
|
||||||
|
var value = string.IsNullOrWhiteSpace(rawValue) ? "-" : rawValue;
|
||||||
|
|
||||||
|
g.DrawString(label, PdfTheme.TableFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.TextSecondary),
|
||||||
|
new RectangleF(x + pad, y + 1f, labelW, rh));
|
||||||
|
|
||||||
|
bool isNeg = value.TrimStart().StartsWith('-') && value != "-";
|
||||||
|
bool isKey = label == "Rendimento Totale";
|
||||||
|
|
||||||
|
var brush = isNeg ? new PdfSolidBrush(PdfTheme.NegativeRed)
|
||||||
|
: isKey ? new PdfSolidBrush(PdfTheme.AccentBlue)
|
||||||
|
: new PdfSolidBrush(PdfTheme.TextPrimary);
|
||||||
|
|
||||||
|
g.DrawString(value, PdfTheme.TableBold, brush,
|
||||||
|
new RectangleF(x + labelW, y + 1f, valueW - pad, rh));
|
||||||
|
|
||||||
|
g.DrawLine(new PdfPen(PdfTheme.SeparatorLine, 0.3f),
|
||||||
|
x, y + rh, x + w, y + rh);
|
||||||
|
|
||||||
|
y += rh;
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ namespace CertReports.Syncfusion.Services.Implementations;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Orchestratore principale: coordina il flusso di generazione report.
|
/// Orchestratore principale: coordina il flusso di generazione report.
|
||||||
///
|
///
|
||||||
/// Flusso:
|
/// Flusso:
|
||||||
/// 1. Recupera dati dal DB (stored procedures)
|
/// 1. Recupera dati dal DB (stored procedures)
|
||||||
/// 2. Renderizza le sezioni PDF (anagrafica, eventi, scenario)
|
/// 2. Renderizza le sezioni PDF (anagrafica, eventi, scenario)
|
||||||
@@ -21,6 +21,7 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
private readonly IPdfMergerService _merger;
|
private readonly IPdfMergerService _merger;
|
||||||
private readonly IPdfCacheService _cache;
|
private readonly IPdfCacheService _cache;
|
||||||
private readonly ILogger<ReportOrchestrator> _logger;
|
private readonly ILogger<ReportOrchestrator> _logger;
|
||||||
|
private readonly ExpiredAnagraficaSectionRenderer _expiredAnagraficaRenderer;
|
||||||
|
|
||||||
public ReportOrchestrator(
|
public ReportOrchestrator(
|
||||||
ICertificateDataService dataService,
|
ICertificateDataService dataService,
|
||||||
@@ -28,7 +29,8 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
IChartSectionRenderer chartRenderer,
|
IChartSectionRenderer chartRenderer,
|
||||||
IPdfMergerService merger,
|
IPdfMergerService merger,
|
||||||
IPdfCacheService cache,
|
IPdfCacheService cache,
|
||||||
ILogger<ReportOrchestrator> logger)
|
ILogger<ReportOrchestrator> logger,
|
||||||
|
ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer)
|
||||||
{
|
{
|
||||||
_dataService = dataService;
|
_dataService = dataService;
|
||||||
_sectionRenderers = sectionRenderers;
|
_sectionRenderers = sectionRenderers;
|
||||||
@@ -36,13 +38,16 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
_merger = merger;
|
_merger = merger;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_expiredAnagraficaRenderer = expiredAnagraficaRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false)
|
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false)
|
||||||
{
|
{
|
||||||
// ── Cache check (chiave include branding) ─────────────────────────
|
// ── Cache check ────────────────────────────────────────────────
|
||||||
var cacheKey = showBranding ? $"{isin}:branded" : isin;
|
var baseCacheKey = showBranding ? $"{isin}:branded" : isin;
|
||||||
var cached = _cache.Get(cacheKey);
|
var expiredCacheKey = showBranding ? $"{isin}:expired:branded" : $"{isin}:expired";
|
||||||
|
|
||||||
|
var cached = _cache.Get(baseCacheKey) ?? _cache.Get(expiredCacheKey);
|
||||||
if (cached != null)
|
if (cached != null)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Report per ISIN {Isin} servito da cache ({Size} bytes)", isin, cached.Length);
|
_logger.LogInformation("Report per ISIN {Isin} servito da cache ({Size} bytes)", isin, cached.Length);
|
||||||
@@ -60,40 +65,75 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
ShowBranding = showBranding,
|
ShowBranding = showBranding,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determina se lo scenario ha dati validi (evita doppia chiamata SP)
|
// ── 2. Determina il tipo di report ────────────────────────────
|
||||||
bool isScenarioAllowed = reportData.Scenario.Rows.Count > 0;
|
bool isExpired = !string.IsNullOrEmpty(reportData.Info.Stato)
|
||||||
|
&& reportData.Info.Stato != "Quotazione";
|
||||||
|
|
||||||
|
var cacheKey = isExpired ? expiredCacheKey : baseCacheKey;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(reportData.Info.Stato))
|
||||||
|
_logger.LogWarning("Campo Stato assente per ISIN {Isin}: utilizzo flusso standard", isin);
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Dati recuperati per {Isin}: {SottostantiCount} sottostanti, {EventiCount} eventi, Scenario: {ScenarioAllowed}",
|
"Dati recuperati per {Isin}: Stato={Stato}, isExpired={IsExpired}, {EventiCount} eventi",
|
||||||
isin, reportData.Info.Sottostanti.Count, reportData.Eventi.Count, isScenarioAllowed);
|
isin, reportData.Info.Stato, isExpired, reportData.Eventi.Count);
|
||||||
|
|
||||||
// ── 2. Genera le sezioni PDF ───────────────────────────────────
|
// ── 3. Genera le sezioni PDF ──────────────────────────────────
|
||||||
var pdfSections = new List<PdfDocument>();
|
var pdfSections = new List<PdfDocument>();
|
||||||
|
|
||||||
foreach (var renderer in _sectionRenderers.OrderBy(r => r.Order))
|
if (isExpired)
|
||||||
{
|
{
|
||||||
// Salta la sezione scenario se il certificato è Protection
|
// Flusso expired: ExpiredAnagrafica + Eventi + Chart
|
||||||
if (renderer.SectionName == "Scenario" && !isScenarioAllowed)
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Sezione Scenario saltata per {Isin} (certificato Protection)", isin);
|
pdfSections.Add(_expiredAnagraficaRenderer.Render(reportData));
|
||||||
continue;
|
_logger.LogInformation("Sezione 'ExpiredAnagrafica' generata per {Isin}", isin);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella sezione 'ExpiredAnagrafica' per {Isin}", isin);
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sectionPdf = renderer.Render(reportData);
|
var eventiRenderer = _sectionRenderers.First(r => r.SectionName == "Eventi");
|
||||||
pdfSections.Add(sectionPdf);
|
pdfSections.Add(eventiRenderer.Render(reportData));
|
||||||
_logger.LogInformation("Sezione '{Section}' generata per {Isin}", renderer.SectionName, isin);
|
_logger.LogInformation("Sezione 'Eventi' generata per {Isin}", isin);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Errore nella generazione della sezione '{Section}' per {Isin}",
|
_logger.LogError(ex, "Errore nella sezione 'Eventi' per {Isin}", isin);
|
||||||
renderer.SectionName, isin);
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Flusso attuale: Anagrafica + Eventi + Scenario (condizionale)
|
||||||
|
bool isScenarioAllowed = reportData.Scenario.Rows.Count > 0;
|
||||||
|
|
||||||
// ── 3. Genera/recupera il grafico ──────────────────────────────
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Grafico (entrambi i flussi) ────────────────────────────
|
||||||
var chartPdf = await _chartRenderer.RenderAsync(isin);
|
var chartPdf = await _chartRenderer.RenderAsync(isin);
|
||||||
if (chartPdf != null)
|
if (chartPdf != null)
|
||||||
{
|
{
|
||||||
@@ -101,20 +141,16 @@ public class ReportOrchestrator : IReportOrchestrator
|
|||||||
_logger.LogInformation("Sezione Grafico aggiunta per {Isin}", isin);
|
_logger.LogInformation("Sezione Grafico aggiunta per {Isin}", isin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 4. Unisci tutto ────────────────────────────────────────────
|
// ── 5. Unisci tutto ────────────────────────────────────────────
|
||||||
var finalPdf = _merger.Merge(pdfSections);
|
var finalPdf = _merger.Merge(pdfSections);
|
||||||
|
|
||||||
_logger.LogInformation("Report generato per {Isin}: {Size} bytes, {Sections} sezioni",
|
_logger.LogInformation("Report generato per {Isin}: {Size} bytes, {Sections} sezioni",
|
||||||
isin, finalPdf.Length, pdfSections.Count);
|
isin, finalPdf.Length, pdfSections.Count);
|
||||||
|
|
||||||
// Salva in cache
|
|
||||||
_cache.Set(cacheKey, finalPdf);
|
_cache.Set(cacheKey, finalPdf);
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
foreach (var doc in pdfSections)
|
foreach (var doc in pdfSections)
|
||||||
{
|
|
||||||
doc.Close(true);
|
doc.Close(true);
|
||||||
}
|
|
||||||
|
|
||||||
return finalPdf;
|
return finalPdf;
|
||||||
}
|
}
|
||||||
|
|||||||
642
docs/superpowers/plans/2026-03-21-expired-certificate-report.md
Normal file
642
docs/superpowers/plans/2026-03-21-expired-certificate-report.md
Normal file
@@ -0,0 +1,642 @@
|
|||||||
|
# Expired Certificate Report — 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 un secondo template PDF per certificati non in quotazione (Scaduto/Rimborsato/Revocato), auto-rilevato dal campo `Stato` della SP esistente, senza modificare endpoint né renderer esistenti.
|
||||||
|
|
||||||
|
**Architecture:** Il campo `Stato` viene aggiunto al modello e mappato dal DB. L'orchestratore legge `Stato` dopo il caricamento dati e sceglie il flusso: attivo (flusso esistente invariato) oppure expired (nuovo `ExpiredAnagraficaSectionRenderer` + `EventiSectionRenderer` riusato + Chart). Il nuovo renderer è iniettato direttamente nell'orchestratore, non via `IEnumerable<IPdfSectionRenderer>`, per evitare inclusione nel ciclo normale.
|
||||||
|
|
||||||
|
**Tech Stack:** ASP.NET Core 8, Syncfusion PDF (Community), SkiaSharp, Microsoft.Data.SqlClient, `dotnet build` per verifica compilazione.
|
||||||
|
|
||||||
|
> **Nota:** Non esistono test automatici nel progetto. La verifica avviene tramite `dotnet build` (compilazione) e richiesta HTTP manuale con un ISIN expired reale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| File | Operazione |
|
||||||
|
|------|-----------|
|
||||||
|
| `CertReports.Syncfusion/Models/CertificateModels.cs` | Modifica — aggiunge campo `Stato` |
|
||||||
|
| `CertReports.Syncfusion/Services/Implementations/CertificateDataService.cs` | Modifica — mappa `Stato` dal reader |
|
||||||
|
| `CertReports.Syncfusion/Services/Implementations/ExpiredAnagraficaSectionRenderer.cs` | **Nuovo file** |
|
||||||
|
| `CertReports.Syncfusion/Services/Implementations/ReportOrchestrator.cs` | Modifica — branching + cache keys + inject ExpiredRenderer |
|
||||||
|
| `CertReports.Syncfusion/Program.cs` | Modifica — registra `ExpiredAnagraficaSectionRenderer` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: Aggiungere campo `Stato` al modello
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `CertReports.Syncfusion/Models/CertificateModels.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Aggiungere la proprietà `Stato` a `CertificateInfo`**
|
||||||
|
|
||||||
|
Aprire `CertificateModels.cs`. Alla fine della sezione `// Vari` (dopo `TriggerOneStar` e prima di `Note`), aggiungere:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Stato quotazione (da SP rpt_Master_CFT_ISIN)
|
||||||
|
// Valori: "Quotazione" | "Revocato" | "Scaduto" | "Rimborsato"
|
||||||
|
public string Stato { get; set; } = string.Empty;
|
||||||
|
```
|
||||||
|
|
||||||
|
La sezione interessata è intorno a riga 73-76:
|
||||||
|
```csharp
|
||||||
|
// Vari
|
||||||
|
public string Leva { get; set; } = string.Empty;
|
||||||
|
public string FattoreAirbag { get; set; } = string.Empty;
|
||||||
|
public string TriggerOneStar { get; set; } = string.Empty;
|
||||||
|
public string Note { get; set; } = string.Empty;
|
||||||
|
```
|
||||||
|
|
||||||
|
Diventa:
|
||||||
|
```csharp
|
||||||
|
// Vari
|
||||||
|
public string Leva { get; set; } = string.Empty;
|
||||||
|
public string FattoreAirbag { get; set; } = string.Empty;
|
||||||
|
public string TriggerOneStar { get; set; } = string.Empty;
|
||||||
|
public string Note { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Stato quotazione (da SP rpt_Master_CFT_ISIN)
|
||||||
|
// Valori: "Quotazione" | "Revocato" | "Scaduto" | "Rimborsato"
|
||||||
|
public string Stato { get; set; } = string.Empty;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verificare compilazione**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd "CertReports.Syncfusion" && dotnet build
|
||||||
|
```
|
||||||
|
Atteso: `Build succeeded` (0 errori).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add CertReports.Syncfusion/Models/CertificateModels.cs
|
||||||
|
git commit -m "feat: add Stato field to CertificateInfo model"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: Mappare `Stato` nel data service
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `CertReports.Syncfusion/Services/Implementations/CertificateDataService.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Aggiungere la mappatura nel reader di `rpt_Master_CFT_ISIN`**
|
||||||
|
|
||||||
|
In `GetCertificateInfoAsync`, dopo la riga che mappa `RendimentoAttuale` (riga ~100), aggiungere:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
info.RendimentoAttuale = r.GetStringSafe("RendimentoAttuale");
|
||||||
|
info.Stato = r.GetStringSafe("Stato"); // ← aggiungere questa riga
|
||||||
|
```
|
||||||
|
|
||||||
|
Il blocco risultante sarà:
|
||||||
|
```csharp
|
||||||
|
info.CpnInMemoria = r.GetNullableDecimal("CpnInMemoria");
|
||||||
|
info.CpnDaPagare = r.GetNullableDecimal("CpnDaPagare");
|
||||||
|
info.CpnPagati = r.GetNullableDecimal("CpnPagati");
|
||||||
|
info.RendimentoAttuale = r.GetStringSafe("RendimentoAttuale");
|
||||||
|
info.Stato = r.GetStringSafe("Stato");
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Nota:** `GetStringSafe` è già nel progetto (extension method su `SqlDataReader`). Se la SP non restituisce il campo `Stato`, il metodo restituisce `string.Empty` senza eccezioni — sicuro.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verificare compilazione**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
Atteso: `Build succeeded`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add CertReports.Syncfusion/Services/Implementations/CertificateDataService.cs
|
||||||
|
git commit -m "feat: map Stato field from rpt_Master_CFT_ISIN SP"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: Creare `ExpiredAnagraficaSectionRenderer`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `CertReports.Syncfusion/Services/Implementations/ExpiredAnagraficaSectionRenderer.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Creare il file con il renderer completo**
|
||||||
|
|
||||||
|
Creare il file `CertReports.Syncfusion/Services/Implementations/ExpiredAnagraficaSectionRenderer.cs` con il seguente contenuto:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sezione 1 — Prima pagina del report per certificati non in quotazione
|
||||||
|
/// (Scaduto, Rimborsato, Revocato). Struttura semplificata rispetto al report attivo:
|
||||||
|
/// solo Titolo + Caratteristiche Prodotto + Analisi. Niente Bid/Ask, niente Sottostanti.
|
||||||
|
/// </summary>
|
||||||
|
public class ExpiredAnagraficaSectionRenderer : IPdfSectionRenderer
|
||||||
|
{
|
||||||
|
public string SectionName => "ExpiredAnagrafica";
|
||||||
|
public int Order => 1;
|
||||||
|
|
||||||
|
private const float PageW = 595f - 2 * PdfTheme.PageMargin;
|
||||||
|
private const float PageH = 842f - 2 * PdfTheme.PageMargin - PdfTheme.FooterHeight;
|
||||||
|
private const float ColGap = 10f;
|
||||||
|
private const float ColW = (PageW - ColGap) / 2f;
|
||||||
|
private const float SectionGap = 10f;
|
||||||
|
|
||||||
|
public PdfDocument Render(CertificateReportData data)
|
||||||
|
{
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
var page = PdfTheme.AddA4Page(doc);
|
||||||
|
var g = page.Graphics;
|
||||||
|
var info = data.Info;
|
||||||
|
float y = 0f;
|
||||||
|
|
||||||
|
// ── TITOLO ────────────────────────────────────────────────────
|
||||||
|
y = DrawTitle(g, info, PageW, y);
|
||||||
|
|
||||||
|
// ── SEZIONE A: CARATTERISTICHE PRODOTTO ───────────────────────
|
||||||
|
y = DrawSectionLabel(g, "Caratteristiche Prodotto", y);
|
||||||
|
y = DrawCaratteristiche(g, info, y);
|
||||||
|
y += SectionGap;
|
||||||
|
|
||||||
|
// ── SEZIONE B: ANALISI ────────────────────────────────────────
|
||||||
|
y = DrawSectionLabel(g, "Analisi", y);
|
||||||
|
DrawAnalisi(g, info, y);
|
||||||
|
|
||||||
|
PdfTheme.DrawFooter(g, PageW, PageH, 1, data.ShowBranding);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// TITOLO — Solo Tipologia box (niente Data/Bid/Ask)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawTitle(PdfGraphics g, CertificateInfo info, float w, float y)
|
||||||
|
{
|
||||||
|
g.DrawString($"Scheda Prodotto {info.Isin}",
|
||||||
|
PdfTheme.SectionTitleFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.AccentBlue),
|
||||||
|
new RectangleF(0, y, w, 20f),
|
||||||
|
new PdfStringFormat(PdfTextAlignment.Center));
|
||||||
|
y += 24f;
|
||||||
|
|
||||||
|
g.DrawLine(PdfTheme.AccentBluePen, 0, y, w, y);
|
||||||
|
y += 8f;
|
||||||
|
|
||||||
|
// Solo box Tipologia, niente Data/Bid/Ask
|
||||||
|
const float boxH = 28f;
|
||||||
|
if (!string.IsNullOrEmpty(info.Categoria))
|
||||||
|
{
|
||||||
|
DrawInfoBox(g, 0, y, w, boxH,
|
||||||
|
"TIPOLOGIA", info.Categoria,
|
||||||
|
PdfTheme.BoxLightBlueBgBrush, PdfTheme.AccentBlueBrush,
|
||||||
|
PdfTheme.AccentBlueBrush, PdfTheme.AccentBlueDarkBrush,
|
||||||
|
PdfTheme.Bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
y += boxH + 10f;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// INTESTAZIONE DI SEZIONE
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawSectionLabel(PdfGraphics g, string title, float y)
|
||||||
|
{
|
||||||
|
g.DrawRectangle(PdfTheme.AccentBlueBrush, new RectangleF(0, y, 3f, 14f));
|
||||||
|
g.DrawString(title, PdfTheme.Bold,
|
||||||
|
new PdfSolidBrush(PdfTheme.AccentBlue),
|
||||||
|
new RectangleF(6f, y, PageW, 14f));
|
||||||
|
return y + 16f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// INFO BOX (identico ad AnagraficaSectionRenderer)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private void DrawInfoBox(
|
||||||
|
PdfGraphics g,
|
||||||
|
float x, float y, float w, float boxH,
|
||||||
|
string label, string value,
|
||||||
|
PdfBrush bgBrush, PdfBrush accentBrush,
|
||||||
|
PdfBrush labelBrush, PdfBrush valueBrush,
|
||||||
|
PdfFont valueFont)
|
||||||
|
{
|
||||||
|
g.DrawRectangle(bgBrush, new RectangleF(x, y, w, boxH));
|
||||||
|
g.DrawRectangle(accentBrush, new RectangleF(x, y, 4f, boxH));
|
||||||
|
float innerX = x + 4f + 6f;
|
||||||
|
float innerW = w - 4f - 6f - 4f;
|
||||||
|
g.DrawString(label, PdfTheme.Small, labelBrush,
|
||||||
|
new RectangleF(innerX, y + 4f, innerW, 10f));
|
||||||
|
g.DrawString(value, valueFont, valueBrush,
|
||||||
|
new RectangleF(innerX, y + 14f, innerW, 14f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// SEZIONE A: CARATTERISTICHE PRODOTTO
|
||||||
|
// Sinistra: tabella emittente con campi rimborso
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawCaratteristiche(PdfGraphics g, CertificateInfo info, float y)
|
||||||
|
{
|
||||||
|
// Tabella occupa la colonna sinistra; destra libera
|
||||||
|
return DrawEmittenteTable(g, info, 0, ColW, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float DrawEmittenteTable(PdfGraphics g, CertificateInfo info, float x, float w, float y)
|
||||||
|
{
|
||||||
|
float rh = PdfTheme.RowHeight;
|
||||||
|
float pad = PdfTheme.CellPadding;
|
||||||
|
|
||||||
|
// Header blu
|
||||||
|
g.DrawRectangle(PdfTheme.TableHeaderBrush, new RectangleF(x, y, w, rh));
|
||||||
|
string headerText = string.IsNullOrEmpty(info.Emittente)
|
||||||
|
? "EMITTENTE"
|
||||||
|
: $"EMITTENTE {info.Emittente.ToUpper()}";
|
||||||
|
g.DrawString(headerText, PdfTheme.TableBold,
|
||||||
|
PdfTheme.HeaderTextBrush,
|
||||||
|
new RectangleF(x + pad, y + 3f, w - pad * 2, rh));
|
||||||
|
y += rh;
|
||||||
|
|
||||||
|
var rows = new[]
|
||||||
|
{
|
||||||
|
("ISIN", info.Isin),
|
||||||
|
("Mercato", info.Mercato),
|
||||||
|
("Valuta", info.Valuta),
|
||||||
|
("Data Emissione", info.DataEmissione),
|
||||||
|
("Valore Rimborso", info.ValoreRimborso),
|
||||||
|
("Data Rimborso", info.DataRimborso),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < rows.Length; i++)
|
||||||
|
{
|
||||||
|
var (label, value) = rows[i];
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) continue;
|
||||||
|
|
||||||
|
bool alt = i % 2 == 1;
|
||||||
|
if (alt)
|
||||||
|
g.DrawRectangle(new PdfSolidBrush(PdfTheme.TableAltRow),
|
||||||
|
new RectangleF(x, y, w, rh));
|
||||||
|
|
||||||
|
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
|
||||||
|
|
||||||
|
g.DrawString(label, PdfTheme.TableFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.TextSecondary),
|
||||||
|
new RectangleF(x + pad, y + 2f, w * 0.5f, rh));
|
||||||
|
|
||||||
|
var valueBrush = label == "ISIN"
|
||||||
|
? new PdfSolidBrush(PdfTheme.AccentBlue)
|
||||||
|
: new PdfSolidBrush(PdfTheme.TextPrimary);
|
||||||
|
|
||||||
|
g.DrawString(value, PdfTheme.TableBold, valueBrush,
|
||||||
|
new RectangleF(x + w * 0.5f, y + 2f, w * 0.5f - pad, rh),
|
||||||
|
new PdfStringFormat(PdfTextAlignment.Right));
|
||||||
|
|
||||||
|
y += rh;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.DrawLine(PdfTheme.TableBorderPen, x, y, x + w, y);
|
||||||
|
return y + 2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// SEZIONE B: ANALISI — KV list colonna destra
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawAnalisi(PdfGraphics g, CertificateInfo info, float y)
|
||||||
|
{
|
||||||
|
var items = new (string Label, string Value)[]
|
||||||
|
{
|
||||||
|
("Importo Cedola (p.a.)", info.NominalAnnualYield),
|
||||||
|
("Frequenza Cedola", info.FrequenzaCedole),
|
||||||
|
("Valore Nominale", info.NominalValue?.ToString("N0") ?? string.Empty),
|
||||||
|
("Memoria Cedola", info.Memory),
|
||||||
|
("Tipo Barriera", info.BarrierType),
|
||||||
|
("Tipo Basket", info.BasketType),
|
||||||
|
("Rendimento Totale", info.RendimentoTotale),
|
||||||
|
};
|
||||||
|
|
||||||
|
return DrawKVList(g, items, ColW + ColGap, ColW, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float DrawKVList(PdfGraphics g, (string Label, string Value)[] items, float x, float w, float y)
|
||||||
|
{
|
||||||
|
float rh = PdfTheme.RowHeight;
|
||||||
|
float pad = PdfTheme.CellPadding;
|
||||||
|
float labelW = w * 0.58f;
|
||||||
|
float valueW = w * 0.42f;
|
||||||
|
|
||||||
|
foreach (var (label, value) in items)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value)) continue;
|
||||||
|
|
||||||
|
g.DrawString(label, PdfTheme.TableFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.TextSecondary),
|
||||||
|
new RectangleF(x + pad, y + 1f, labelW, rh));
|
||||||
|
|
||||||
|
bool isNeg = value.TrimStart().StartsWith('-');
|
||||||
|
bool isKey = label == "Rendimento Totale";
|
||||||
|
|
||||||
|
var brush = isNeg ? new PdfSolidBrush(PdfTheme.NegativeRed)
|
||||||
|
: isKey ? new PdfSolidBrush(PdfTheme.AccentBlue)
|
||||||
|
: new PdfSolidBrush(PdfTheme.TextPrimary);
|
||||||
|
|
||||||
|
g.DrawString(value, PdfTheme.TableBold, brush,
|
||||||
|
new RectangleF(x + labelW, y + 1f, valueW - pad, rh),
|
||||||
|
new PdfStringFormat(PdfTextAlignment.Right));
|
||||||
|
|
||||||
|
g.DrawLine(new PdfPen(PdfTheme.SeparatorLine, 0.3f),
|
||||||
|
x, y + rh, x + w, y + rh);
|
||||||
|
|
||||||
|
y += rh;
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verificare compilazione**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
Atteso: `Build succeeded`. Se manca un `using`, aggiungerlo. Riferimento per i `using` necessari: guardare `AnagraficaSectionRenderer.cs` (stessi namespace).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add CertReports.Syncfusion/Services/Implementations/ExpiredAnagraficaSectionRenderer.cs
|
||||||
|
git commit -m "feat: add ExpiredAnagraficaSectionRenderer for non-quoted certificates"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Aggiornare `ReportOrchestrator` — branching + cache + injection
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `CertReports.Syncfusion/Services/Implementations/ReportOrchestrator.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Aggiungere `ExpiredAnagraficaSectionRenderer` al costruttore**
|
||||||
|
|
||||||
|
Il costruttore attuale riceve `IEnumerable<IPdfSectionRenderer> sectionRenderers`. Aggiungere `ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer` come parametro diretto:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
private readonly ExpiredAnagraficaSectionRenderer _expiredAnagraficaRenderer;
|
||||||
|
|
||||||
|
public ReportOrchestrator(
|
||||||
|
ICertificateDataService dataService,
|
||||||
|
IEnumerable<IPdfSectionRenderer> sectionRenderers,
|
||||||
|
IChartSectionRenderer chartRenderer,
|
||||||
|
IPdfMergerService merger,
|
||||||
|
IPdfCacheService cache,
|
||||||
|
ILogger<ReportOrchestrator> logger,
|
||||||
|
ExpiredAnagraficaSectionRenderer expiredAnagraficaRenderer) // ← aggiunto
|
||||||
|
{
|
||||||
|
_dataService = dataService;
|
||||||
|
_sectionRenderers = sectionRenderers;
|
||||||
|
_chartRenderer = chartRenderer;
|
||||||
|
_merger = merger;
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
|
_expiredAnagraficaRenderer = expiredAnagraficaRenderer; // ← aggiunto
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Aggiornare `GenerateReportAsync` — cache key e branching**
|
||||||
|
|
||||||
|
Sostituire la logica `GenerateReportAsync` (dal `var cacheKey` in poi) con questa:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public async Task<byte[]> GenerateReportAsync(string isin, bool showBranding = false)
|
||||||
|
{
|
||||||
|
// ── Cache check ────────────────────────────────────────────────
|
||||||
|
// La chiave include branding E tipo report. Il tipo verrà determinato
|
||||||
|
// dopo il caricamento dati, ma usiamo una chiave provvisoria per
|
||||||
|
// il primo lookup — se c'è cache, non leggiamo nemmeno il tipo.
|
||||||
|
// Strategia: chiave separata per expired dopo determinazione tipo.
|
||||||
|
// Per semplicità: tentativo cache con chiave base prima del DB,
|
||||||
|
// poi con chiave tipizzata dopo la lettura Stato.
|
||||||
|
|
||||||
|
// Prima proviamo con la chiave "standard" — hit solo se già cached
|
||||||
|
var baseCacheKey = showBranding ? $"{isin}:branded" : isin;
|
||||||
|
var expiredCacheKey = showBranding ? $"{isin}:expired:branded" : $"{isin}:expired";
|
||||||
|
|
||||||
|
// Controlla entrambe le chiavi (una sarà un miss, l'altra un possibile hit)
|
||||||
|
var cached = _cache.Get(baseCacheKey) ?? _cache.Get(expiredCacheKey);
|
||||||
|
if (cached != null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Report per ISIN {Isin} servito da cache ({Size} bytes)", isin, cached.Length);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Inizio generazione report per ISIN {Isin}", isin);
|
||||||
|
|
||||||
|
// ── 1. Recupera tutti i dati ───────────────────────────────────
|
||||||
|
var reportData = new CertificateReportData
|
||||||
|
{
|
||||||
|
Info = await _dataService.GetCertificateInfoAsync(isin),
|
||||||
|
Eventi = await _dataService.GetCertificateEventsAsync(isin),
|
||||||
|
Scenario = await _dataService.GetScenarioAnalysisAsync(isin),
|
||||||
|
ShowBranding = showBranding,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 2. Determina il tipo di report ────────────────────────────
|
||||||
|
bool isExpired = !string.IsNullOrEmpty(reportData.Info.Stato)
|
||||||
|
&& reportData.Info.Stato != "Quotazione";
|
||||||
|
|
||||||
|
var cacheKey = isExpired ? expiredCacheKey : baseCacheKey;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Dati recuperati per {Isin}: Stato={Stato}, isExpired={IsExpired}, {EventiCount} eventi",
|
||||||
|
isin, reportData.Info.Stato, isExpired, reportData.Eventi.Count);
|
||||||
|
|
||||||
|
// ── 3. Genera le sezioni PDF ──────────────────────────────────
|
||||||
|
var pdfSections = new List<PdfDocument>();
|
||||||
|
|
||||||
|
if (isExpired)
|
||||||
|
{
|
||||||
|
// Flusso expired: ExpiredAnagrafica + Eventi + Chart
|
||||||
|
pdfSections.Add(_expiredAnagraficaRenderer.Render(reportData));
|
||||||
|
_logger.LogInformation("Sezione 'ExpiredAnagrafica' generata per {Isin}", isin);
|
||||||
|
|
||||||
|
var eventiRenderer = _sectionRenderers.First(r => r.SectionName == "Eventi");
|
||||||
|
pdfSections.Add(eventiRenderer.Render(reportData));
|
||||||
|
_logger.LogInformation("Sezione 'Eventi' generata per {Isin}", isin);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Flusso attuale: Anagrafica + Eventi + Scenario (condizionale)
|
||||||
|
bool isScenarioAllowed = reportData.Scenario.Rows.Count > 0;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Grafico (entrambi i flussi) ────────────────────────────
|
||||||
|
var chartPdf = await _chartRenderer.RenderAsync(isin);
|
||||||
|
if (chartPdf != null)
|
||||||
|
{
|
||||||
|
pdfSections.Add(chartPdf);
|
||||||
|
_logger.LogInformation("Sezione Grafico aggiunta per {Isin}", isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 5. Unisci tutto ────────────────────────────────────────────
|
||||||
|
var finalPdf = _merger.Merge(pdfSections);
|
||||||
|
|
||||||
|
_logger.LogInformation("Report generato per {Isin}: {Size} bytes, {Sections} sezioni",
|
||||||
|
isin, finalPdf.Length, pdfSections.Count);
|
||||||
|
|
||||||
|
_cache.Set(cacheKey, finalPdf);
|
||||||
|
|
||||||
|
foreach (var doc in pdfSections)
|
||||||
|
doc.Close(true);
|
||||||
|
|
||||||
|
return finalPdf;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Verificare compilazione**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
Atteso: `Build succeeded`. Se appare errore su `_expiredAnagraficaRenderer`, verificare che il campo privato e il costruttore siano entrambi aggiornati.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add CertReports.Syncfusion/Services/Implementations/ReportOrchestrator.cs
|
||||||
|
git commit -m "feat: add expired certificate report branching in orchestrator"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5: Registrare `ExpiredAnagraficaSectionRenderer` in DI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `CertReports.Syncfusion/Program.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Aggiungere la registrazione**
|
||||||
|
|
||||||
|
In `Program.cs`, nel blocco dove sono registrati gli altri renderer (dopo le registrazioni `AddScoped<IPdfSectionRenderer, ...>`), aggiungere:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Registrazione diretta (non come IPdfSectionRenderer) —
|
||||||
|
// l'orchestratore la inietta direttamente per il flusso expired.
|
||||||
|
builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
|
||||||
|
```
|
||||||
|
|
||||||
|
La sezione registrazioni renderer in `Program.cs` sarà simile a:
|
||||||
|
```csharp
|
||||||
|
builder.Services.AddScoped<IPdfSectionRenderer, AnagraficaSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<IPdfSectionRenderer, EventiSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<IPdfSectionRenderer, ScenarioSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>(); // ← aggiunta
|
||||||
|
builder.Services.AddScoped<IChartSectionRenderer, ChartSectionRenderer>();
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verificare compilazione finale**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
Atteso: `Build succeeded` senza warning critici.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add CertReports.Syncfusion/Program.cs
|
||||||
|
git commit -m "feat: register ExpiredAnagraficaSectionRenderer in DI"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6: Verifica manuale end-to-end
|
||||||
|
|
||||||
|
- [ ] **Step 1: Avviare l'applicazione**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet run --project CertReports.Syncfusion
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Testare con un ISIN expired**
|
||||||
|
|
||||||
|
Aprire nel browser o usare curl con un ISIN reale nello stato "Scaduto"/"Rimborsato"/"Revocato":
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://localhost:{porta}/api/report/by-isin/IT0006745704
|
||||||
|
```
|
||||||
|
|
||||||
|
Atteso:
|
||||||
|
- PDF a 3 pagine (non 4)
|
||||||
|
- Pagina 1: titolo + solo box Tipologia (niente Bid/Ask) + tabella con Valore Rimborso e Data Rimborso + KV Analisi con 7 campi
|
||||||
|
- Pagina 2: Lista eventi (identica all'attuale)
|
||||||
|
- Pagina 3: Grafico
|
||||||
|
|
||||||
|
- [ ] **Step 3: Testare branding**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://localhost:{porta}/api/report/by-isin/IT0006745704?branding=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Atteso: footer con "Powered by Smart Roots" su tutte le 3 pagine.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Testare che il report attivo non sia rotto**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://localhost:{porta}/api/report/by-isin/{ISIN_ATTIVO}
|
||||||
|
```
|
||||||
|
|
||||||
|
Atteso: report attivo invariato a 4 pagine (o 3 senza scenario).
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit finale**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v-expired-report-complete
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note di implementazione
|
||||||
|
|
||||||
|
**Brush e font disponibili in `PdfTheme`** (già esistenti, non creare nuovi):
|
||||||
|
- `PdfTheme.AccentBlue`, `PdfTheme.AccentBlueBrush`, `PdfTheme.AccentBluePen`
|
||||||
|
- `PdfTheme.AccentBlueDarkBrush`, `PdfTheme.BoxLightBlueBgBrush`
|
||||||
|
- `PdfTheme.TableHeaderBrush`, `PdfTheme.HeaderTextBrush`, `PdfTheme.TableAltRowBrush`
|
||||||
|
- `PdfTheme.TableAltRow` (Color), `PdfTheme.TextSecondary`, `PdfTheme.TextPrimary`
|
||||||
|
- `PdfTheme.NegativeRed`, `PdfTheme.SeparatorLine`
|
||||||
|
- `PdfTheme.TableBorderPen`
|
||||||
|
- Font: `PdfTheme.SectionTitleFont`, `PdfTheme.Bold`, `PdfTheme.Small`, `PdfTheme.TableFont`, `PdfTheme.TableBold`
|
||||||
|
- `PdfTheme.RowHeight`, `PdfTheme.CellPadding`, `PdfTheme.PageMargin`, `PdfTheme.FooterHeight`
|
||||||
|
- `PdfTheme.AddA4Page(doc)`, `PdfTheme.DrawFooter(g, w, h, pageNum, showBranding)`
|
||||||
|
|
||||||
|
**Cache lookup doppio:** Il lookup `_cache.Get(baseCacheKey) ?? _cache.Get(expiredCacheKey)` nel nuovo orchestratore è necessario perché non conosciamo il tipo del certificato prima di leggere il DB. Il caching avviene poi con la chiave corretta basata su `Stato`. Questo comporta al più 2 miss di cache prima della prima generazione, poi 1 hit per le richieste successive — accettabile.
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Design Spec: Report Certificati Non in Quotazione (Expired)
|
||||||
|
|
||||||
|
**Data:** 2026-03-21
|
||||||
|
**Progetto:** SmartReports / CertReports.Syncfusion
|
||||||
|
**Autore:** Brainstorming session
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Obiettivo
|
||||||
|
|
||||||
|
Implementare un secondo template di report PDF per certificati non più in quotazione (Stato: Revocato, Scaduto, Rimborsato). Il report mantiene lo stesso stile grafico del report attivo ma mostra meno informazioni: 3 pagine totali (Anagrafica semplificata + Lista Eventi + Grafico). Niente Scenario. Il rilevamento del tipo è automatico tramite il campo `Stato` restituito dalla SP esistente `rpt_Master_CFT_ISIN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Struttura del Report Expired (3 pagine)
|
||||||
|
|
||||||
|
| Pagina | Renderer | Contenuto |
|
||||||
|
|--------|----------|-----------|
|
||||||
|
| 1 | `ExpiredAnagraficaSectionRenderer` (nuovo) | Titolo + Caratteristiche + Analisi |
|
||||||
|
| 2 | `EventiSectionRenderer` (riusato) | Lista eventi (identica all'attuale) |
|
||||||
|
| 3 | `ChartSectionRenderer` (riusato) | Grafico performance (identico all'attuale) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modifiche al Modello
|
||||||
|
|
||||||
|
### `CertificateInfo` — aggiungere un campo
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public string Stato { get; set; } = string.Empty;
|
||||||
|
// Valori possibili: "Quotazione" | "Revocato" | "Scaduto" | "Rimborsato"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `CertificateDataService` — mappare il nuovo campo
|
||||||
|
|
||||||
|
Nel metodo che chiama `rpt_Master_CFT_ISIN`, aggiungere la mappatura:
|
||||||
|
```csharp
|
||||||
|
Stato = reader["Stato"]?.ToString() ?? string.Empty
|
||||||
|
```
|
||||||
|
|
||||||
|
Nessuna altra modifica alle query o agli altri servizi dati.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nuovo Renderer: `ExpiredAnagraficaSectionRenderer`
|
||||||
|
|
||||||
|
**File:** `CertReports.Syncfusion/Services/Implementations/ExpiredAnagraficaSectionRenderer.cs`
|
||||||
|
**Interfaccia:** `IPdfSectionRenderer`
|
||||||
|
**Order:** 1
|
||||||
|
**SectionName:** `"ExpiredAnagrafica"`
|
||||||
|
|
||||||
|
### Layout Pagina 1 (Portrait A4)
|
||||||
|
|
||||||
|
#### Blocco Titolo
|
||||||
|
- "Scheda Prodotto {ISIN}" — stesso stile del titolo attuale (font grande, colore AccentBlue)
|
||||||
|
- Riga sotto: "Tipologia: {Categoria}" — font normale
|
||||||
|
- **Niente** Bid/Ask, niente LastPriceDate
|
||||||
|
- Linea separatrice orizzontale blu (come attuale)
|
||||||
|
|
||||||
|
#### Blocco Caratteristiche Prodotto (tabella sinistra, stessa grafica attuale)
|
||||||
|
|
||||||
|
| Label | Campo modello |
|
||||||
|
|-------|---------------|
|
||||||
|
| EMITTENTE | `Info.Emittente` |
|
||||||
|
| ISIN | `Info.Isin` |
|
||||||
|
| Mercato | `Info.Mercato` |
|
||||||
|
| Valuta | `Info.Valuta` |
|
||||||
|
| Data Emissione | `Info.DataEmissione` |
|
||||||
|
| Valore Rimborso | `Info.ValoreRimborso` |
|
||||||
|
| Data Rimborso | `Info.DataRimborso` |
|
||||||
|
|
||||||
|
#### Blocco Analisi (KV, stessa grafica attuale)
|
||||||
|
|
||||||
|
| Label | Campo modello |
|
||||||
|
|-------|---------------|
|
||||||
|
| Importo Cedola (p.a.) | `Info.NominalAnnualYield` |
|
||||||
|
| Frequenza Cedola | `Info.FrequenzaCedole` |
|
||||||
|
| Valore Nominale | `Info.NominalValue` |
|
||||||
|
| Memoria Cedola | `Info.Memory` |
|
||||||
|
| Tipo Barriera | `Info.BarrierType` |
|
||||||
|
| Tipo Basket | `Info.BasketType` |
|
||||||
|
| Rendimento Totale | `Info.RendimentoTotale` |
|
||||||
|
|
||||||
|
#### Escluso dalla pagina 1
|
||||||
|
- Sezione Bid/Ask/LastPriceDate
|
||||||
|
- Tabella Sottostanti (sezione C del report attivo)
|
||||||
|
- Sezione cedole (CpnPagati/DaPagare/InMemoria)
|
||||||
|
- Sezione Analisi (8+9 KV del report attivo)
|
||||||
|
|
||||||
|
### Footer
|
||||||
|
`PdfTheme.DrawFooter(g, pageWidth, pageHeight, pageNumber, showBranding)` — identico a tutti gli altri renderer, supporta il parametro branding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modifiche all'Orchestratore
|
||||||
|
|
||||||
|
### `ReportOrchestrator.cs`
|
||||||
|
|
||||||
|
Dopo il caricamento dei dati (`CertificateDataService`), leggere `data.Info.Stato` per scegliere il flusso:
|
||||||
|
|
||||||
|
**Strategia isExpired — denylist:** il check usa `!= "Quotazione"` (denylist), così qualsiasi nuovo stato non previsto viene trattato come expired piuttosto che come attivo. Se in futuro si aggiungono stati con report differenti, si sostituirà con un allowlist esplicito.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
bool isExpired = !string.IsNullOrEmpty(data.Info.Stato) && data.Info.Stato != "Quotazione";
|
||||||
|
// Copre: "Revocato", "Scaduto", "Rimborsato" — e qualsiasi stato futuro non-attivo
|
||||||
|
|
||||||
|
if (isExpired)
|
||||||
|
{
|
||||||
|
// Flusso expired: ExpiredAnagrafica + Eventi + Chart
|
||||||
|
var eventiRenderer = _sectionRenderers.First(r => r.SectionName == "Eventi");
|
||||||
|
pdfSections.Add(_expiredAnagraficaRenderer.Render(data));
|
||||||
|
pdfSections.Add(eventiRenderer.Render(data));
|
||||||
|
// Chart: await _chartRenderer.RenderAsync(data.Info.Isin) — aggiunto se non null
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Flusso attuale: foreach _sectionRenderers.OrderBy(r => r.Order) — logica esistente invariata
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Come si ottiene EventiSectionRenderer nel flusso expired:** tramite `_sectionRenderers.First(r => r.SectionName == "Eventi")`. È già registrato come `IPdfSectionRenderer` e presente nella collection iniettata.
|
||||||
|
|
||||||
|
**Firma ChartRenderer:** `await _chartRenderer.RenderAsync(data.Info.Isin)` — stessa chiamata del flusso attivo, restituisce `PdfDocument?` (null se il grafico non è disponibile).
|
||||||
|
|
||||||
|
Il renderer `ExpiredAnagraficaSectionRenderer` è iniettato direttamente nell'orchestratore (non via `IEnumerable<IPdfSectionRenderer>`) per evitare che venga incluso nel ciclo del flusso normale.
|
||||||
|
|
||||||
|
### Cache — chiavi separate
|
||||||
|
|
||||||
|
Le chiavi sono passate a `_cache.Get/Set` a livello orchestratore; `PdfCacheService.CacheKey()` aggiunge poi il prefisso `report_pdf_` internamente (comportamento invariato per entrambi i flussi).
|
||||||
|
|
||||||
|
| Scenario | Chiave passata all'orchestratore | Chiave effettiva in memoria |
|
||||||
|
|----------|----------------------------------|-----------------------------|
|
||||||
|
| Attivo, no branding | `{isin}` | `report_pdf_{isin}` |
|
||||||
|
| Attivo, branding | `{isin}:branded` | `report_pdf_{isin}:branded` |
|
||||||
|
| Expired, no branding | `{isin}:expired` | `report_pdf_{isin}:expired` |
|
||||||
|
| Expired, branding | `{isin}:expired:branded` | `report_pdf_{isin}:expired:branded` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branding
|
||||||
|
|
||||||
|
Il parametro `?branding=true` funziona identicamente al report attivo:
|
||||||
|
- Propagato come `CertificateReportData.ShowBranding`
|
||||||
|
- Passato a `PdfTheme.DrawFooter(..., showBranding)` in ogni pagina del report expired
|
||||||
|
- Footer sinistra: "Powered by " + hyperlink "Smart Roots" → `https://www.smart-roots.net`
|
||||||
|
- Footer destra: numero pagina
|
||||||
|
- Cache usa chiave `{isin}:expired:branded` vs `{isin}:expired`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependency Injection — `Program.cs`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Registrazione diretta (non come IPdfSectionRenderer) per evitare inclusione nel ciclo normale
|
||||||
|
builder.Services.AddScoped<ExpiredAnagraficaSectionRenderer>();
|
||||||
|
```
|
||||||
|
|
||||||
|
Gli altri renderer esistenti rimangono invariati.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Endpoint
|
||||||
|
|
||||||
|
**Nessun nuovo endpoint.** Il rilevamento del tipo report è trasparente al chiamante. Tutti gli endpoint esistenti funzionano per entrambi i tipi:
|
||||||
|
|
||||||
|
| Endpoint | Comportamento |
|
||||||
|
|----------|---------------|
|
||||||
|
| `GET /api/report?p={encrypted}` | Auto-rileva da Stato |
|
||||||
|
| `GET /api/report?alias={id}` | Auto-rileva da Stato |
|
||||||
|
| `GET /api/report/by-isin/{isin}` | Auto-rileva da Stato |
|
||||||
|
| `GET /api/report/download?p={...}` | Auto-rileva da Stato |
|
||||||
|
| `GET /api/report/download?alias={...}` | Auto-rileva da Stato |
|
||||||
|
| Tutti | Supportano `?branding=true` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Modificati / Creati
|
||||||
|
|
||||||
|
| File | Tipo modifica |
|
||||||
|
|------|---------------|
|
||||||
|
| `Models/CertificateModels.cs` | Aggiunta campo `Stato` a `CertificateInfo` |
|
||||||
|
| `Services/Implementations/CertificateDataService.cs` | Mappatura campo `Stato` dalla SP |
|
||||||
|
| `Services/Implementations/ExpiredAnagraficaSectionRenderer.cs` | **Nuovo file** |
|
||||||
|
| `Services/Implementations/ReportOrchestrator.cs` | Logica branching expired/attivo + cache keys |
|
||||||
|
| `Program.cs` | Registrazione `ExpiredAnagraficaSectionRenderer` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non modificati
|
||||||
|
|
||||||
|
- `AnagraficaSectionRenderer.cs` — invariato
|
||||||
|
- `EventiSectionRenderer.cs` — riusato as-is
|
||||||
|
- `ScenarioSectionRenderer.cs` — invariato (semplicemente non incluso nel flusso expired)
|
||||||
|
- `ChartSectionRenderer.cs` — riusato as-is
|
||||||
|
- `PdfTheme.cs` — invariato
|
||||||
|
- `ReportController.cs` — invariato
|
||||||
|
- Tutti gli endpoint — invariati
|
||||||
Reference in New Issue
Block a user