643 lines
25 KiB
Markdown
643 lines
25 KiB
Markdown
# 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.
|