chore: initial commit - baseline before redesign
This commit is contained in:
19
CertReports.Syncfusion/.gitignore
vendored
Normal file
19
CertReports.Syncfusion/.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## .NET
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.vs/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
Logs/
|
||||||
|
|
||||||
|
## Temp files
|
||||||
|
wwwroot/temp/
|
||||||
|
|
||||||
|
## Environment-specific
|
||||||
|
appsettings.*.local.json
|
||||||
|
|
||||||
|
## Secrets (mai committare!)
|
||||||
|
appsettings.Production.json
|
||||||
28
CertReports.Syncfusion/CertReports.Syncfusion.csproj
Normal file
28
CertReports.Syncfusion/CertReports.Syncfusion.csproj
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>CertReports.Syncfusion</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Syncfusion PDF -->
|
||||||
|
<PackageReference Include="Syncfusion.Pdf.Net.Core" Version="33.1.44" />
|
||||||
|
|
||||||
|
<!-- Syncfusion PDF Graphics (per disegno avanzato, testi, forme) -->
|
||||||
|
<!-- <PackageReference Include="Syncfusion.Drawing.Net.Core" Version="27.*" /> -->
|
||||||
|
|
||||||
|
<!-- SQL Server -->
|
||||||
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
|
||||||
|
|
||||||
|
<!-- SkiaSharp per generazione chart server-side -->
|
||||||
|
<PackageReference Include="SkiaSharp" Version="3.119.2" />
|
||||||
|
|
||||||
|
<!-- Logging -->
|
||||||
|
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
109
CertReports.Syncfusion/Controllers/ChartController.cs
Normal file
109
CertReports.Syncfusion/Controllers/ChartController.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
using CertReports.Syncfusion.Services.Implementations;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API per la generazione standalone del grafico certificato.
|
||||||
|
/// Richiamabile da qualsiasi progetto esterno.
|
||||||
|
///
|
||||||
|
/// Endpoint:
|
||||||
|
/// GET /api/chart/{isin} → PNG inline
|
||||||
|
/// GET /api/chart/{isin}?format=png → PNG inline
|
||||||
|
/// GET /api/chart/{isin}?format=pdf → PDF inline
|
||||||
|
/// GET /api/chart/{isin}?width=1100&height=700&format=png
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class ChartController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IChartDataService _chartDataService;
|
||||||
|
private readonly ILogger<ChartController> _logger;
|
||||||
|
|
||||||
|
public ChartController(IChartDataService chartDataService, ILogger<ChartController> logger)
|
||||||
|
{
|
||||||
|
_chartDataService = chartDataService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("{isin}")]
|
||||||
|
public async Task<IActionResult> GenerateChart(
|
||||||
|
string isin,
|
||||||
|
[FromQuery] int width = 1100,
|
||||||
|
[FromQuery] int height = 700,
|
||||||
|
[FromQuery] string format = "png")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(isin))
|
||||||
|
return BadRequest("ISIN non valido.");
|
||||||
|
|
||||||
|
// Limiti ragionevoli
|
||||||
|
width = Math.Clamp(width, 400, 2000);
|
||||||
|
height = Math.Clamp(height, 300, 1500);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var chartData = await _chartDataService.GetChartDataAsync(isin);
|
||||||
|
|
||||||
|
if (chartData == null || chartData.Series.Count == 0)
|
||||||
|
{
|
||||||
|
return NotFound(new
|
||||||
|
{
|
||||||
|
status = "KO",
|
||||||
|
message = $"Nessun dato per il grafico di {isin} (meno di 30 prezzi EOD?)."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genera PNG
|
||||||
|
byte[] pngBytes = SkiaChartRenderer.RenderToPng(chartData, width, height);
|
||||||
|
|
||||||
|
if (format.Equals("pdf", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Converti in PDF landscape
|
||||||
|
byte[] pdfBytes = WrapPngInPdf(pngBytes);
|
||||||
|
Response.Headers.Append("Content-Disposition", $"inline; filename=chart_{isin}.pdf");
|
||||||
|
return File(pdfBytes, "application/pdf");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: PNG
|
||||||
|
Response.Headers.Append("Content-Disposition", $"inline; filename=chart_{isin}.png");
|
||||||
|
return File(pngBytes, "image/png");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore generazione chart per ISIN {Isin}", isin);
|
||||||
|
return StatusCode(500, new { status = "KO", message = "Errore nella generazione del grafico." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] WrapPngInPdf(byte[] pngBytes)
|
||||||
|
{
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
doc.PageSettings.Size = PdfPageSize.A4;
|
||||||
|
doc.PageSettings.Orientation = PdfPageOrientation.Landscape;
|
||||||
|
doc.PageSettings.Margins.All = 30;
|
||||||
|
|
||||||
|
var page = doc.Pages.Add();
|
||||||
|
var g = page.Graphics;
|
||||||
|
float pw = page.GetClientSize().Width;
|
||||||
|
float ph = page.GetClientSize().Height;
|
||||||
|
|
||||||
|
using var stream = new MemoryStream(pngBytes);
|
||||||
|
var img = new PdfBitmap(stream);
|
||||||
|
|
||||||
|
float ratio = (float)img.Width / img.Height;
|
||||||
|
float dw = pw;
|
||||||
|
float dh = dw / ratio;
|
||||||
|
if (dh > ph) { dh = ph; dw = dh * ratio; }
|
||||||
|
|
||||||
|
float x = (pw - dw) / 2;
|
||||||
|
float y = (ph - dh) / 2;
|
||||||
|
g.DrawImage(img, x, y, dw, dh);
|
||||||
|
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
doc.Save(output);
|
||||||
|
doc.Close(true);
|
||||||
|
return output.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
139
CertReports.Syncfusion/Controllers/ReportController.cs
Normal file
139
CertReports.Syncfusion/Controllers/ReportController.cs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controller REST per la generazione dei report certificati.
|
||||||
|
/// Sostituisce la vecchia WebForm ReportFSSiteCrypt.aspx.
|
||||||
|
///
|
||||||
|
/// Endpoint:
|
||||||
|
/// GET /api/report?p={encrypted_isin}
|
||||||
|
/// GET /api/report?alias={alias_id}
|
||||||
|
/// GET /api/report/by-isin/{isin} (uso interno/debug)
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class ReportController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IReportOrchestrator _orchestrator;
|
||||||
|
private readonly ICertificateDataService _dataService;
|
||||||
|
private readonly CryptoHelper _crypto;
|
||||||
|
private readonly ILogger<ReportController> _logger;
|
||||||
|
|
||||||
|
public ReportController(
|
||||||
|
IReportOrchestrator orchestrator,
|
||||||
|
ICertificateDataService dataService,
|
||||||
|
CryptoHelper crypto,
|
||||||
|
ILogger<ReportController> logger)
|
||||||
|
{
|
||||||
|
_orchestrator = orchestrator;
|
||||||
|
_dataService = dataService;
|
||||||
|
_crypto = crypto;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint principale - compatibile con la vecchia URL.
|
||||||
|
/// Accetta parametri 'p' (ISIN cifrato) o 'alias' (alias ID).
|
||||||
|
/// Restituisce il PDF inline (visualizzabile nel browser).
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> GenerateReport(
|
||||||
|
[FromQuery(Name = "p")] string? encryptedIsin = null,
|
||||||
|
[FromQuery(Name = "alias")] string? aliasId = null)
|
||||||
|
{
|
||||||
|
string? isin = null;
|
||||||
|
|
||||||
|
// Risolvi ISIN
|
||||||
|
if (!string.IsNullOrEmpty(encryptedIsin))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
isin = _crypto.DecryptIsin(encryptedIsin);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Errore nella decodifica ISIN cifrato");
|
||||||
|
return BadRequest("Parametro 'p' non valido.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(aliasId))
|
||||||
|
{
|
||||||
|
isin = await _dataService.FindIsinByAliasIdAsync(aliasId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(isin))
|
||||||
|
{
|
||||||
|
return BadRequest("Specificare il parametro 'p' (ISIN cifrato) o 'alias' (alias ID).");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GenerateAndReturnPdf(isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint diretto per ISIN (uso interno / debug).
|
||||||
|
/// In produzione proteggere con autenticazione.
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("by-isin/{isin}")]
|
||||||
|
public async Task<IActionResult> GenerateReportByIsin(string isin)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(isin) || isin.Length < 12)
|
||||||
|
{
|
||||||
|
return BadRequest("ISIN non valido.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GenerateAndReturnPdf(isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint per download come allegato (content-disposition: attachment)
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("download")]
|
||||||
|
public async Task<IActionResult> DownloadReport(
|
||||||
|
[FromQuery(Name = "p")] string? encryptedIsin = null,
|
||||||
|
[FromQuery(Name = "alias")] string? aliasId = null)
|
||||||
|
{
|
||||||
|
string? isin = null;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(encryptedIsin))
|
||||||
|
isin = _crypto.DecryptIsin(encryptedIsin);
|
||||||
|
else if (!string.IsNullOrEmpty(aliasId))
|
||||||
|
isin = await _dataService.FindIsinByAliasIdAsync(aliasId);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(isin))
|
||||||
|
return BadRequest("Specificare il parametro 'p' o 'alias'.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var pdfBytes = await _orchestrator.GenerateReportAsync(isin);
|
||||||
|
return File(pdfBytes, "application/pdf", $"{isin}.pdf");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore generazione report download per ISIN {Isin}", isin);
|
||||||
|
return StatusCode(500, "Errore nella generazione del report.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helper ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private async Task<IActionResult> GenerateAndReturnPdf(string isin)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Richiesta report per ISIN {Isin}", isin);
|
||||||
|
var pdfBytes = await _orchestrator.GenerateReportAsync(isin);
|
||||||
|
|
||||||
|
// Inline: il PDF si apre direttamente nel browser
|
||||||
|
Response.Headers.Append("Content-Disposition", $"inline; filename={isin}.pdf");
|
||||||
|
return File(pdfBytes, "application/pdf");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore generazione report per ISIN {Isin}", isin);
|
||||||
|
return StatusCode(500, "Errore nella generazione del report.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
CertReports.Syncfusion/Controllers/TestController.cs
Normal file
47
CertReports.Syncfusion/Controllers/TestController.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class TestController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public TestController(IConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("db")]
|
||||||
|
public async Task<IActionResult> TestDb()
|
||||||
|
{
|
||||||
|
var connStr = _config.GetConnectionString("CertDb");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(connStr);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
await using var cmd = new SqlCommand("SELECT 1", conn);
|
||||||
|
var result = await cmd.ExecuteScalarAsync();
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
status = "OK",
|
||||||
|
result = result?.ToString(),
|
||||||
|
connectionString = connStr?.Replace(_config["ConnectionStrings:CertDb"]?.Split("Password=").LastOrDefault()?.Split(";").FirstOrDefault() ?? "", "***")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
status = "ERRORE",
|
||||||
|
message = ex.Message,
|
||||||
|
innerMessage = ex.InnerException?.Message,
|
||||||
|
connectionString = connStr
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
426
CertReports.Syncfusion/DOCS.md
Normal file
426
CertReports.Syncfusion/DOCS.md
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
# CertReports.Syncfusion — Documentazione Tecnica
|
||||||
|
|
||||||
|
> Progetto ASP.NET Core 8 per la generazione di report PDF per certificati finanziari strutturati.
|
||||||
|
> Sostituisce il vecchio progetto WebForms basato su DevExpress (non licenziato) + PdfSharp.
|
||||||
|
> Utilizza **Syncfusion PDF Library** (Community License) e **SkiaSharp** per i grafici.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Panoramica
|
||||||
|
|
||||||
|
Il sistema riceve un codice ISIN (in chiaro, cifrato o tramite alias), interroga il database SQL Server tramite stored procedures, genera un report PDF composto da 4 sezioni e lo restituisce come stream inline al browser.
|
||||||
|
|
||||||
|
### Flusso di generazione
|
||||||
|
|
||||||
|
```
|
||||||
|
Richiesta HTTP (ISIN)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ReportController ──→ decodifica ISIN (se cifrato)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ReportOrchestrator
|
||||||
|
│
|
||||||
|
├──→ CertificateDataService
|
||||||
|
│ ├── rpt_Master_CFT_ISIN → Anagrafica certificato
|
||||||
|
│ ├── rpt_Details_UL_ISIN → Sottostanti
|
||||||
|
│ ├── rpt_Events_CFT_ISIN → Eventi
|
||||||
|
│ └── rpt_AnalisiScenario_ISIN → Analisi scenario
|
||||||
|
│
|
||||||
|
├──→ AnagraficaSectionRenderer → PDF Sezione 1
|
||||||
|
├──→ EventiSectionRenderer → PDF Sezione 2
|
||||||
|
├──→ ScenarioSectionRenderer → PDF Sezione 3 (se dati presenti)
|
||||||
|
│
|
||||||
|
├──→ ChartDataService
|
||||||
|
│ ├── FSWeb_Chart_UL → Info sottostanti grafico
|
||||||
|
│ ├── FSWeb_Chart_DailyCTF → Performance certificato
|
||||||
|
│ └── FSWeb_Chart_DailyUL → Performance sottostanti
|
||||||
|
│
|
||||||
|
├──→ ChartSectionRenderer → PDF Sezione 4 (SkiaSharp)
|
||||||
|
│ └── SkiaChartRenderer → Genera PNG in memoria
|
||||||
|
│
|
||||||
|
└──→ PdfMergerService → Unisce i PDF → byte[]
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Response PDF inline
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Struttura del progetto
|
||||||
|
|
||||||
|
```
|
||||||
|
SmartReports/ ← Solution
|
||||||
|
└── CertReports.Syncfusion/ ← Progetto
|
||||||
|
├── Controllers/
|
||||||
|
│ ├── ReportController.cs ← API report PDF completo
|
||||||
|
│ └── ChartController.cs ← API chart standalone (PNG/PDF)
|
||||||
|
│
|
||||||
|
├── Helpers/
|
||||||
|
│ ├── CryptoHelper.cs ← Decodifica ISIN (ENCRYPTBYPASSPHRASE)
|
||||||
|
│ ├── GlobalExceptionMiddleware.cs ← Error handling centralizzato
|
||||||
|
│ ├── HealthChecks.cs ← Health check DB + Chart service
|
||||||
|
│ └── PdfTheme.cs ← Colori, font, stili, bordi (tema centralizzato)
|
||||||
|
│
|
||||||
|
├── Models/
|
||||||
|
│ ├── CertificateModels.cs ← Modelli report (anagrafica, eventi, scenario)
|
||||||
|
│ └── ChartModels.cs ← Modelli grafico (serie, punti, barriere)
|
||||||
|
│
|
||||||
|
├── Services/
|
||||||
|
│ ├── Interfaces/
|
||||||
|
│ │ └── IServices.cs ← Contratti DI per tutti i servizi
|
||||||
|
│ └── Implementations/
|
||||||
|
│ ├── CertificateDataService.cs ← Accesso DB: anagrafica, eventi, scenario
|
||||||
|
│ ├── ChartDataService.cs ← Accesso DB: dati grafico (3 SP)
|
||||||
|
│ ├── AnagraficaSectionRenderer.cs ← Sezione 1: anagrafica + sottostanti
|
||||||
|
│ ├── EventiSectionRenderer.cs ← Sezione 2: tabella eventi (multi-pagina)
|
||||||
|
│ ├── ScenarioSectionRenderer.cs ← Sezione 3: matrice scenario con gradiente
|
||||||
|
│ ├── ChartSectionRenderer.cs ← Sezione 4: inserisce chart PNG nel PDF
|
||||||
|
│ ├── SkiaChartRenderer.cs ← Genera il grafico con SkiaSharp
|
||||||
|
│ ├── PdfMergerService.cs ← Merge PDF con Syncfusion
|
||||||
|
│ ├── PdfCacheService.cs ← Cache in memoria (IMemoryCache)
|
||||||
|
│ └── ReportOrchestrator.cs ← Coordinatore principale
|
||||||
|
│
|
||||||
|
├── Program.cs ← Entry point, DI, middleware
|
||||||
|
├── GlobalUsings.cs ← Using globali
|
||||||
|
├── CertReports.Syncfusion.csproj ← Progetto + pacchetti NuGet
|
||||||
|
├── appsettings.json ← Configurazione produzione
|
||||||
|
├── appsettings.Development.json ← Override per sviluppo
|
||||||
|
├── Dockerfile ← Build containerizzata
|
||||||
|
├── docker-compose.yml ← Compose con SQL Server
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Mapping vecchio → nuovo
|
||||||
|
|
||||||
|
| Vecchio (DevExpress / WebForms) | Nuovo (Syncfusion / ASP.NET Core 8) |
|
||||||
|
|----------------------------------------------|-------------------------------------------|
|
||||||
|
| `ReportFSSiteCrypt.aspx` (Page_Load) | `ReportController.cs` (API REST) |
|
||||||
|
| `XtraReport` + file `.repx` | Classi `IPdfSectionRenderer` (codice C#) |
|
||||||
|
| `report.ExportToPdf()` per ogni sezione | Ogni renderer restituisce `PdfDocument` |
|
||||||
|
| `PdfSharp.PdfReader.Open` + `CopyPages` | `PdfMergerService` (Syncfusion) |
|
||||||
|
| `CommonClass.DecryptCombined()` | `CryptoHelper.DecryptIsin()` |
|
||||||
|
| `CommonClass.execSP_Scalar()` | `CertificateDataService` (async) |
|
||||||
|
| `ChartFSWeb.aspx` (DevExpress WebChart) | `SkiaChartRenderer` + `ChartController` |
|
||||||
|
| `CallWebApi()` con `WebRequest` | Generazione interna (nessuna chiamata HTTP)|
|
||||||
|
| File temporanei su disco + `File.Delete` | Tutto in memoria (`MemoryStream`) |
|
||||||
|
| `Response.BinaryWrite()` | `return File(bytes, "application/pdf")` |
|
||||||
|
| `System.Data.SqlClient` | `Microsoft.Data.SqlClient` v5 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Stored procedures utilizzate
|
||||||
|
|
||||||
|
Tutte le SP sono nel database `FirstSolutionDB`.
|
||||||
|
|
||||||
|
### Report (sezioni 1-3)
|
||||||
|
|
||||||
|
| SP | Parametro | Sezione | Descrizione |
|
||||||
|
|----|-----------|---------|-------------|
|
||||||
|
| `rpt_Master_CFT_ISIN` | `@ISIN varchar(12)` | 1 — Anagrafica | Dati certificato (1 record). Campi pre-formattati con `FORMAT()`. Join su Issuer, Category, BasketType, BarrierType + numerose `OUTER APPLY` per valori calcolati. |
|
||||||
|
| `rpt_Details_UL_ISIN` | `@ISIN varchar(12)` | 1 — Sottostanti | N record sottostanti con strike, last price, barriere, buffer, dividendi. Campi formattati con `FORMAT(...,'N3','it-IT')`. |
|
||||||
|
| `rpt_Events_CFT_ISIN` | `@ISIN varchar(50)` | 2 — Eventi | Lista eventi ordinata per data. Cedole, trigger, autocall, importi pagati. Campi formattati. Colonne Trigger Capitale e Valore Capitale escluse dal report. |
|
||||||
|
| `rpt_AnalisiScenario_ISIN` | `@ISIN varchar(50)` | 3 — Scenario | 7 righe UNION con label in `Descrizione` e 13 colonne di valori (`col1..col13`). Variazioni: -90%, -80%, -70%, -60%, -50%, -40%, -30%, -20%, -10%, 0%, +10%, +20%, +30%. Tutte stringhe pre-formattate. |
|
||||||
|
| `rpt_FindIsinbyAliasID` | `@AliasID` | Risoluzione | Restituisce l'ISIN dato un alias ID. |
|
||||||
|
|
||||||
|
### Grafico (sezione 4)
|
||||||
|
|
||||||
|
| SP | Parametri | Descrizione |
|
||||||
|
|----|-----------|-------------|
|
||||||
|
| `FSWeb_Chart_UL` | `@isin varchar(15)` | Info sottostanti per il grafico: IDCertificates, IDUnderlyings, StartDate, Strike, BarrieraCoupon, BarrieraCapitale, Nome. Restituisce dati solo se ci sono almeno 30 prezzi EOD. |
|
||||||
|
| `FSWeb_Chart_DailyCTF` | `@ISIN varchar(50)` | Performance giornaliera del certificato: `Px_date`, `Performance` (= PX_LAST_EOD / Nominal_amount × 100). |
|
||||||
|
| `FSWeb_Chart_DailyUL` | `@IDCertificates, @IDUnderlyings, @StartDate, @Strike` | Performance giornaliera di ogni sottostante: `Px_date`, `Performance` (= Px_close / Strike × 100). Usa Px_closeadj se AdjustedPrices=1. Filtra per date presenti nel certificato. |
|
||||||
|
|
||||||
|
### Nota sui dati formattati
|
||||||
|
|
||||||
|
Le SP restituiscono la maggior parte dei valori **già formattati come stringhe** (es. `FORMAT(value,'P2','it-IT')`, `FORMAT(value,'F2','it-IT')`). I modelli C# usano `string` per questi campi per evitare parsing e riformattazione non necessari. Solo `NominalValue`, `PrezzoEmissione`, `CpnPagati`, `CpnDaPagare`, `CpnInMemoria` sono `decimal?` perché servono come valori numerici nel rendering.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Endpoint API
|
||||||
|
|
||||||
|
### Generazione report PDF completo (inline nel browser)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/report?p={isin_cifrato}
|
||||||
|
GET /api/report?alias={alias_id}
|
||||||
|
GET /api/report/by-isin/{isin}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Download report come allegato
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/report/download?p={isin_cifrato}
|
||||||
|
GET /api/report/download?alias={alias_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grafico standalone (richiamabile da altri progetti)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/chart/{isin} → PNG inline
|
||||||
|
GET /api/chart/{isin}?format=png → PNG inline
|
||||||
|
GET /api/chart/{isin}?format=pdf → PDF inline
|
||||||
|
GET /api/chart/{isin}?width=1200&height=800 → Dimensioni custom
|
||||||
|
```
|
||||||
|
|
||||||
|
Parametri opzionali: `width` (400-2000, default 1100), `height` (300-1500, default 700), `format` (png/pdf, default png).
|
||||||
|
|
||||||
|
### Health check
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /health
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Configurazione
|
||||||
|
|
||||||
|
### appsettings.json
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"CertDb": "Data Source=INDIRIZZO_SERVER;Initial Catalog=FirstSolutionDB;Persist Security Info=True;User ID=sa;Password=***;TrustServerCertificate=True;Encrypt=False;"
|
||||||
|
},
|
||||||
|
"Syncfusion": {
|
||||||
|
"LicenseKey": "CHIAVE_COMMUNITY_SYNCFUSION"
|
||||||
|
},
|
||||||
|
"CryptoSettings": {
|
||||||
|
"Passphrase": "ddCE3hM9BNJXgwtj"
|
||||||
|
},
|
||||||
|
"ReportSettings": {
|
||||||
|
"CacheMinutes": 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La sezione `ChartService` non è più necessaria: il grafico viene generato internamente.
|
||||||
|
|
||||||
|
### Note sulla connection string
|
||||||
|
|
||||||
|
- Usare `Data Source` (non `Server`) e `Initial Catalog` (non `Database`) per compatibilità con `Microsoft.Data.SqlClient` v5
|
||||||
|
- Aggiungere `Encrypt=False;` se il SQL Server non ha SSL configurato
|
||||||
|
- Se la connessione non funziona via TCP, aggiungere il prefisso `tcp:` all'indirizzo (es. `Data Source=tcp:26.69.45.60;`)
|
||||||
|
|
||||||
|
### appsettings.Development.json
|
||||||
|
|
||||||
|
Non deve contenere `ConnectionStrings` — altrimenti sovrascrive i valori di `appsettings.json`. Contiene solo override per logging e cache in sviluppo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Pacchetti NuGet
|
||||||
|
|
||||||
|
| Pacchetto | Versione | Scopo |
|
||||||
|
|-----------|----------|-------|
|
||||||
|
| `Syncfusion.Pdf.Net.Core` | 33.x | Generazione e merge PDF |
|
||||||
|
| `Microsoft.Data.SqlClient` | 5.x | Accesso SQL Server |
|
||||||
|
| `SkiaSharp` | 2.x | Generazione grafico server-side |
|
||||||
|
| `Serilog.AspNetCore` | 8.x | Logging strutturato |
|
||||||
|
| `Serilog.Sinks.File` | 5.x | Log su file con rotazione giornaliera |
|
||||||
|
|
||||||
|
### Note
|
||||||
|
|
||||||
|
- `Syncfusion.Drawing.Net.Core` **non esiste più** nella v33: è inglobato in `Syncfusion.Pdf.Net.Core`
|
||||||
|
- Per i colori usare `Color.FromArgb(255, r, g, b)` invece di `new Color(r, g, b)` (cambiamento v33)
|
||||||
|
- `PdfStandardFont` **non è IDisposable** in Syncfusion v33: non usare `using` nella dichiarazione
|
||||||
|
- SkiaSharp v2.x: usare `SKFont` per dimensione/typeface e `canvas.DrawText(text, x, y, SKTextAlign, font, paint)` — le API su `SKPaint` sono obsolete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Architettura e pattern
|
||||||
|
|
||||||
|
### Dependency Injection
|
||||||
|
|
||||||
|
Tutti i servizi sono registrati in `Program.cs`:
|
||||||
|
|
||||||
|
- `ICertificateDataService` → `CertificateDataService` (Scoped)
|
||||||
|
- `IChartDataService` → `ChartDataService` (Scoped)
|
||||||
|
- `IPdfSectionRenderer` → registrati multipli: `Anagrafica`, `Eventi`, `Scenario` (Scoped)
|
||||||
|
- `IChartSectionRenderer` → `ChartSectionRenderer` (Scoped)
|
||||||
|
- `IPdfMergerService` → `PdfMergerService` (Scoped)
|
||||||
|
- `IReportOrchestrator` → `ReportOrchestrator` (Scoped)
|
||||||
|
- `IPdfCacheService` → `PdfCacheService` (Singleton)
|
||||||
|
- `CryptoHelper` (Singleton)
|
||||||
|
|
||||||
|
### Aggiungere una nuova sezione al report
|
||||||
|
|
||||||
|
1. Creare una classe che implementa `IPdfSectionRenderer`
|
||||||
|
2. Assegnare `SectionName` e `Order` (determina la posizione nel PDF)
|
||||||
|
3. Implementare `Render(CertificateReportData data)` che restituisce un `PdfDocument`
|
||||||
|
4. Registrarla in `Program.cs`: `builder.Services.AddScoped<IPdfSectionRenderer, NuovaSezione>();`
|
||||||
|
|
||||||
|
L'orchestratore la includerà automaticamente nel report, ordinata per `Order`.
|
||||||
|
|
||||||
|
### Tema e stili
|
||||||
|
|
||||||
|
`PdfTheme.cs` centralizza:
|
||||||
|
|
||||||
|
- **Colori**: `HeaderBackground`, `AlternateRow`, `PositiveValue`, `NegativeValue`, ecc.
|
||||||
|
- **Font**: `Regular` (8pt), `Bold`, `Small` (6.5pt), `Title` (14pt), `Header` (7pt bold)
|
||||||
|
- **Layout**: `PageMargin` (40pt), `RowHeight` (18pt), `CellPadding` (4pt)
|
||||||
|
- **Brushes e Pens**: pronti all'uso, derivati dai colori
|
||||||
|
- **Bordi**: `ApplyThinBorders(grid, thickness)` per linee tabella sottili e uniformi
|
||||||
|
|
||||||
|
Modificare un valore in `PdfTheme` aggiorna tutti i renderer automaticamente.
|
||||||
|
|
||||||
|
### Bordi tabelle
|
||||||
|
|
||||||
|
Tutte le tabelle PdfGrid usano `PdfTheme.ApplyThinBorders(grid)` prima del `Draw()` per ottenere linee sottili e uniformi (0.25pt di default). Il metodo itera su headers e righe applicando `PdfBorders` con `PdfPen` del colore e spessore definiti nel tema.
|
||||||
|
|
||||||
|
### Cache
|
||||||
|
|
||||||
|
I PDF generati vengono memorizzati in `IMemoryCache` per `CacheMinutes` (default 5 minuti, configurabile). Evita rigenerazioni per ricaricamenti di pagina. La cache ha un limite di 200 MB totali.
|
||||||
|
|
||||||
|
### Gestione errori
|
||||||
|
|
||||||
|
`GlobalExceptionMiddleware` cattura tutte le eccezioni non gestite e restituisce risposte JSON coerenti con status code appropriati (400, 404, 408, 500).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Sezione grafico (Sezione 4)
|
||||||
|
|
||||||
|
Il grafico viene generato **interamente in memoria** con SkiaSharp, eliminando la dipendenza dall'endpoint esterno `ChartFSWeb.aspx`.
|
||||||
|
|
||||||
|
### Architettura
|
||||||
|
|
||||||
|
```
|
||||||
|
ChartSectionRenderer
|
||||||
|
│
|
||||||
|
├──→ ChartDataService ← Recupera dati da DB (3 SP)
|
||||||
|
│
|
||||||
|
└──→ SkiaChartRenderer ← Genera PNG con SkiaSharp
|
||||||
|
│
|
||||||
|
└──→ PNG in memoria → inserito nel PDF come PdfBitmap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cosa disegna il grafico
|
||||||
|
|
||||||
|
- **Serie certificato**: linea nera, spessore 2.5px (performance % nel tempo)
|
||||||
|
- **Serie sottostanti**: N linee colorate, spessore 1.5px (ciascuna con colore diverso dal palette ciclico)
|
||||||
|
- **Barriera Capitale**: linea orizzontale rossa
|
||||||
|
- **Barriera Coupon**: linea orizzontale viola (solo se diversa da Barriera Capitale)
|
||||||
|
- **Strike (100%)**: linea orizzontale verde
|
||||||
|
- **Asse Y**: percentuale con auto-scaling e margine 10%
|
||||||
|
- **Asse X**: date con step automatico (trimestrale/semestrale/annuale)
|
||||||
|
- **Legenda**: box in alto a destra con tutte le serie e le linee costanti
|
||||||
|
- **Griglia**: linee orizzontali e verticali grigio chiaro
|
||||||
|
|
||||||
|
### API chart standalone
|
||||||
|
|
||||||
|
Il `ChartController` espone il grafico come API indipendente, richiamabile da qualsiasi altro progetto senza passare per il report completo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Sezione scenario (Sezione 3)
|
||||||
|
|
||||||
|
### Stile visivo
|
||||||
|
|
||||||
|
La sezione scenario replica lo stile del vecchio report DevExpress:
|
||||||
|
|
||||||
|
- **Header con gradiente colore**: da rosso scuro (-90%) attraverso arancio e giallo fino a verde (+30%)
|
||||||
|
- **Testo header bianco** su sfondo colorato
|
||||||
|
- **Titolo "Analisi Scenario"** centrato, grande, colore blu
|
||||||
|
- **Righe alternate** con sfondo grigio chiaro
|
||||||
|
- **Valori negativi** in rosso scuro
|
||||||
|
- **13 colonne di variazione**: -90%, -80%, -70%, -60%, -50%, -40%, -30%, -20%, -10%, 0%, +10%, +20%, +30%
|
||||||
|
|
||||||
|
### Logica di inclusione
|
||||||
|
|
||||||
|
La sezione scenario viene inclusa nel report se `rpt_AnalisiScenario_ISIN` restituisce almeno una riga. La condizione è semplicemente `scenario.Rows.Count > 0` (non controlla i valori, perché la SP restituisce anche `"--"` per dati non calcolabili).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Deploy
|
||||||
|
|
||||||
|
### Locale (Visual Studio)
|
||||||
|
|
||||||
|
```
|
||||||
|
F5 → https://localhost:{porta}/api/report/by-isin/{ISIN}
|
||||||
|
F5 → https://localhost:{porta}/api/chart/{ISIN}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
|
# → http://localhost:5080/api/report/by-isin/{ISIN}
|
||||||
|
# → http://localhost:5080/api/chart/{ISIN}
|
||||||
|
```
|
||||||
|
|
||||||
|
Il `Dockerfile` include l'installazione dei font necessari per il rendering PDF e SkiaSharp (`libfontconfig1`, `fonts-dejavu-core`) e gira come utente non-root per sicurezza.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Logging
|
||||||
|
|
||||||
|
Serilog scrive su:
|
||||||
|
|
||||||
|
- **Console**: tutti i log in tempo reale
|
||||||
|
- **File**: `Logs/certreports-{data}.log` con rotazione giornaliera
|
||||||
|
|
||||||
|
Livelli configurabili in `appsettings.json`. In Development il livello di default è `Debug`, in Production è `Information`.
|
||||||
|
|
||||||
|
Ogni step del flusso è loggato:
|
||||||
|
|
||||||
|
```
|
||||||
|
[INF] Richiesta report per ISIN CH1277653163
|
||||||
|
[DBG] Cache MISS per ISIN CH1277653163
|
||||||
|
[INF] Inizio generazione report per ISIN CH1277653163
|
||||||
|
[INF] Anagrafica caricata per CH1277653163: Leonteq, 3 sottostanti
|
||||||
|
[INF] Eventi caricati per CH1277653163: 36 eventi
|
||||||
|
[INF] Scenario caricato per CH1277653163: 7 righe
|
||||||
|
[INF] Sezione 'Anagrafica' generata per CH1277653163
|
||||||
|
[INF] Sezione 'Eventi' generata per CH1277653163
|
||||||
|
[INF] Sezione 'Scenario' generata per CH1277653163
|
||||||
|
[INF] Dati grafico caricati per CH1277653163: 4 serie, certificato 650 punti
|
||||||
|
[INF] Grafico generato per CH1277653163: 82450 bytes PNG, 4 serie
|
||||||
|
[INF] Sezione Grafico aggiunta per CH1277653163
|
||||||
|
[INF] Report generato per CH1277653163: 125000 bytes, 4 sezioni
|
||||||
|
[DBG] Cache SET per CH1277653163 (125000 bytes, TTL 5min)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Problemi noti e soluzioni
|
||||||
|
|
||||||
|
| Problema | Causa | Soluzione |
|
||||||
|
|----------|-------|-----------|
|
||||||
|
| `Cannot access a closed Stream` nel merge | Gli stream dei `PdfLoadedDocument` venivano chiusi prima del `Save()` finale | `PdfMergerService` tiene tutti gli stream aperti fino al salvataggio, poi fa cleanup nel `finally` |
|
||||||
|
| `Color does not contain a constructor that takes 3 arguments` | Syncfusion v33 ha rimosso `new Color(r,g,b)` | Usare `Color.FromArgb(255, r, g, b)` |
|
||||||
|
| `Syncfusion.Drawing.Net.Core` non trovato | Pacchetto rimosso nella v33, inglobato in `Syncfusion.Pdf.Net.Core` | Rimuovere dal `.csproj` |
|
||||||
|
| `Unexpected character '{'` in `FormatPercent` | Interpolazione annidata non supportata in C# | Usare `value.Value.ToString($"N{decimals}") + " %"` |
|
||||||
|
| Connessione DB via Named Pipes | `Microsoft.Data.SqlClient` v5 tenta Named Pipes di default | Aggiungere `Encrypt=False;` e/o prefisso `tcp:` nella connection string |
|
||||||
|
| Connection string ignorata | `appsettings.Development.json` sovrascriveva `appsettings.json` | Rimuovere `ConnectionStrings` da `appsettings.Development.json` |
|
||||||
|
| Scenario mai generato | SP sbagliata (`rpt_AnalisiRischio_ISIN` con 11 colonne) e check troppo restrittivo | Usare `rpt_AnalisiScenario_ISIN` (13 colonne, label in `Descrizione`), condizione `Rows.Count > 0` |
|
||||||
|
| Namespace conflict `Syncfusion.Pdf` in `ChartController` | `CertReports.Syncfusion.Pdf` confuso con `Syncfusion.Pdf` | Aggiungere `using Syncfusion.Pdf;` esplicito, non usare riferimenti fully-qualified |
|
||||||
|
| `PdfStandardFont` non è IDisposable | Syncfusion v33 non implementa `IDisposable` su questo tipo | Non usare `using` nella dichiarazione |
|
||||||
|
| SkiaSharp `DrawText` obsoleto | `SKPaint.TextSize/Typeface` deprecati in SkiaSharp 2.x | Usare `SKFont` + `canvas.DrawText(text, x, y, SKTextAlign, font, paint)` |
|
||||||
|
| Bordi tabelle troppo spessi | `PdfGrid` usa bordi di default grossi | `PdfTheme.ApplyThinBorders(grid)` con `PdfPen` a 0.25pt prima di `Draw()` |
|
||||||
|
| `grid.Headers` non iterabile con foreach | Syncfusion v33 non espone `IEnumerable` sugli headers | Usare `for (int r = 0; r < grid.Headers.Count; r++)` con accesso indicizzato |
|
||||||
|
| `grid.Draw()` restituisce void | Syncfusion v33 ha rimosso il valore di ritorno | Stimare altezza con `(grid.Rows.Count + 1) * RowHeight` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Cronologia modifiche
|
||||||
|
|
||||||
|
| Data | Modifica |
|
||||||
|
|------|----------|
|
||||||
|
| 18/03/2026 | Creazione progetto iniziale con struttura ASP.NET Core 8 + Syncfusion |
|
||||||
|
| 18/03/2026 | Implementazione completa `CryptoHelper.DecryptCombined` (v1 TripleDES + v2 AES) |
|
||||||
|
| 18/03/2026 | Mapping SP reali: `rpt_Master_CFT_ISIN`, `rpt_Details_UL_ISIN`, `rpt_Events_CFT_ISIN` |
|
||||||
|
| 18/03/2026 | Fix compatibilità Syncfusion v33: `Color.FromArgb`, rimozione `Syncfusion.Drawing.Net.Core` |
|
||||||
|
| 18/03/2026 | Fix `PdfMergerService`: stream mantenuti aperti fino al `Save()` finale |
|
||||||
|
| 18/03/2026 | Fix connection string: `Data Source` + `Encrypt=False` per `Microsoft.Data.SqlClient` v5 |
|
||||||
|
| 18/03/2026 | Fix `appsettings.Development.json` che sovrascriveva la connection string |
|
||||||
|
| 18/03/2026 | Prima generazione PDF riuscita con dati reali |
|
||||||
|
| 18/03/2026 | Porting grafico da DevExpress `ChartFSWeb.aspx` a SkiaSharp interno |
|
||||||
|
| 18/03/2026 | Aggiunta `ChartDataService` (SP: `FSWeb_Chart_UL`, `FSWeb_Chart_DailyCTF`, `FSWeb_Chart_DailyUL`) |
|
||||||
|
| 18/03/2026 | Aggiunta `SkiaChartRenderer` con serie, barriere, legenda, auto-scaling |
|
||||||
|
| 18/03/2026 | Aggiunta `ChartController` per API chart standalone (`/api/chart/{isin}`) |
|
||||||
|
| 18/03/2026 | Fix scenario: SP corretta `rpt_AnalisiScenario_ISIN` con 13 colonne e label `Descrizione` |
|
||||||
|
| 18/03/2026 | Scenario: header con gradiente rosso→verde, valori negativi in rosso |
|
||||||
|
| 18/03/2026 | Rimozione colonne Trigger Capitale e Valore Capitale dalla tabella eventi |
|
||||||
|
| 18/03/2026 | Fix bordi tabelle: `PdfTheme.ApplyThinBorders()` con linee 0.25pt |
|
||||||
|
| 18/03/2026 | Fix SkiaSharp: migrazione a API moderna `SKFont` (rimozione warning obsoleti) |
|
||||||
36
CertReports.Syncfusion/Dockerfile
Normal file
36
CertReports.Syncfusion/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# ── Build stage ──────────────────────────────────────────────────────
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY *.csproj .
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet publish -c Release -o /app/publish --no-restore
|
||||||
|
|
||||||
|
# ── Runtime stage ────────────────────────────────────────────────────
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installa font per il rendering PDF (Syncfusion ne ha bisogno)
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y --no-install-recommends \
|
||||||
|
libfontconfig1 \
|
||||||
|
fonts-dejavu-core && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Crea directory per i log
|
||||||
|
RUN mkdir -p /app/Logs
|
||||||
|
|
||||||
|
COPY --from=build /app/publish .
|
||||||
|
|
||||||
|
# Non-root user per sicurezza
|
||||||
|
RUN groupadd -r appuser && useradd -r -g appuser appuser && \
|
||||||
|
chown -R appuser:appuser /app
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||||
|
|
||||||
|
ENTRYPOINT ["dotnet", "CertReports.Syncfusion.dll"]
|
||||||
4
CertReports.Syncfusion/GlobalUsings.cs
Normal file
4
CertReports.Syncfusion/GlobalUsings.cs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// GlobalUsings.cs
|
||||||
|
// Importazioni globali usate in tutto il progetto
|
||||||
|
global using Syncfusion.Pdf;
|
||||||
|
global using PdfLoadedDocument = Syncfusion.Pdf.Parsing.PdfLoadedDocument;
|
||||||
133
CertReports.Syncfusion/Helpers/CryptoHelper.cs
Normal file
133
CertReports.Syncfusion/Helpers/CryptoHelper.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper per decodifica ISIN cifrato.
|
||||||
|
/// Compatibile con SQL Server ENCRYPTBYPASSPHRASE (versione 1 = TripleDES, versione 2 = AES).
|
||||||
|
/// Portato da CommonClass.DecryptCombined.
|
||||||
|
/// </summary>
|
||||||
|
public class CryptoHelper
|
||||||
|
{
|
||||||
|
private readonly string _passphrase;
|
||||||
|
|
||||||
|
public CryptoHelper(IConfiguration config)
|
||||||
|
{
|
||||||
|
_passphrase = config["CryptoSettings:Passphrase"]
|
||||||
|
?? throw new InvalidOperationException("CryptoSettings:Passphrase non configurata.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decodifica una stringa esadecimale cifrata con SQL Server ENCRYPTBYPASSPHRASE.
|
||||||
|
/// Formato input: "0x0200000047..." (stringa hex con prefisso 0x).
|
||||||
|
/// </summary>
|
||||||
|
public string DecryptIsin(string fromSql)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(fromSql))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return DecryptCombined(fromSql, _passphrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implementazione completa della decodifica ENCRYPTBYPASSPHRASE di SQL Server.
|
||||||
|
/// Supporta versione 1 (TripleDES/SHA1) e versione 2 (AES/SHA256).
|
||||||
|
/// </summary>
|
||||||
|
private static string DecryptCombined(string fromSql, string password)
|
||||||
|
{
|
||||||
|
// Password codificata come UTF16-LE (come fa SQL Server internamente)
|
||||||
|
byte[] passwordBytes = Encoding.Unicode.GetBytes(password);
|
||||||
|
|
||||||
|
// Rimuove il prefisso "0x"
|
||||||
|
if (fromSql.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
fromSql = fromSql[2..];
|
||||||
|
|
||||||
|
// I primi 4 byte (8 caratteri hex) indicano la versione
|
||||||
|
int version = BitConverter.ToInt32(HexToBytes(fromSql[..8]), 0);
|
||||||
|
|
||||||
|
byte[] encrypted;
|
||||||
|
HashAlgorithm hashAlgo;
|
||||||
|
SymmetricAlgorithm cryptoAlgo;
|
||||||
|
int keySize;
|
||||||
|
|
||||||
|
if (version == 1)
|
||||||
|
{
|
||||||
|
// Versione 1: TripleDES + SHA1
|
||||||
|
keySize = 16;
|
||||||
|
hashAlgo = SHA1.Create();
|
||||||
|
cryptoAlgo = TripleDES.Create();
|
||||||
|
cryptoAlgo.IV = HexToBytes(fromSql.Substring(8, 16)); // 8 byte IV = 16 hex chars
|
||||||
|
encrypted = HexToBytes(fromSql[24..]); // Resto = dati cifrati
|
||||||
|
}
|
||||||
|
else if (version == 2)
|
||||||
|
{
|
||||||
|
// Versione 2: AES + SHA256
|
||||||
|
keySize = 32;
|
||||||
|
hashAlgo = SHA256.Create();
|
||||||
|
cryptoAlgo = Aes.Create();
|
||||||
|
cryptoAlgo.IV = HexToBytes(fromSql.Substring(8, 32)); // 16 byte IV = 32 hex chars
|
||||||
|
encrypted = HexToBytes(fromSql[40..]); // Resto = dati cifrati
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new CryptographicException($"Versione di cifratura non supportata: {version}");
|
||||||
|
}
|
||||||
|
|
||||||
|
cryptoAlgo.Padding = PaddingMode.PKCS7;
|
||||||
|
cryptoAlgo.Mode = CipherMode.CBC;
|
||||||
|
|
||||||
|
// Genera la chiave dall'hash della password
|
||||||
|
hashAlgo.TransformFinalBlock(passwordBytes, 0, passwordBytes.Length);
|
||||||
|
cryptoAlgo.Key = hashAlgo.Hash!.Take(keySize).ToArray();
|
||||||
|
|
||||||
|
// Decrittazione
|
||||||
|
byte[] decrypted = cryptoAlgo.CreateDecryptor()
|
||||||
|
.TransformFinalBlock(encrypted, 0, encrypted.Length);
|
||||||
|
|
||||||
|
// Validazione magic number (i primi 4 byte devono essere 0xBAADF00D)
|
||||||
|
uint magic = BitConverter.ToUInt32(decrypted, 0);
|
||||||
|
if (magic != 0xBAADF00D)
|
||||||
|
{
|
||||||
|
throw new CryptographicException(
|
||||||
|
"Decrittazione fallita: magic number non valido. Password errata?");
|
||||||
|
}
|
||||||
|
|
||||||
|
// I byte 4-5 riservati, byte 6-7 = lunghezza, byte 8+ = dati
|
||||||
|
byte[] decryptedData = decrypted[8..];
|
||||||
|
|
||||||
|
// Rileva encoding: se contiene byte 0x00 è UTF-16, altrimenti UTF-8
|
||||||
|
bool isUtf16 = Array.IndexOf(decryptedData, (byte)0) != -1;
|
||||||
|
string result = isUtf16
|
||||||
|
? Encoding.Unicode.GetString(decryptedData)
|
||||||
|
: Encoding.UTF8.GetString(decryptedData);
|
||||||
|
|
||||||
|
// Cleanup risorse crittografiche
|
||||||
|
hashAlgo.Dispose();
|
||||||
|
cryptoAlgo.Dispose();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converte una stringa esadecimale in byte array.
|
||||||
|
/// </summary>
|
||||||
|
private static byte[] HexToBytes(string hex)
|
||||||
|
{
|
||||||
|
byte[] bytes = new byte[hex.Length / 2];
|
||||||
|
for (int i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Genera una stringa casuale sicura.
|
||||||
|
/// </summary>
|
||||||
|
public static string RandomString(int length)
|
||||||
|
{
|
||||||
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
return RandomNumberGenerator.GetString(chars, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
84
CertReports.Syncfusion/Helpers/GlobalExceptionMiddleware.cs
Normal file
84
CertReports.Syncfusion/Helpers/GlobalExceptionMiddleware.cs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Middleware globale per la gestione degli errori.
|
||||||
|
/// Cattura tutte le eccezioni non gestite e restituisce una risposta JSON coerente.
|
||||||
|
/// </summary>
|
||||||
|
public class GlobalExceptionMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<GlobalExceptionMiddleware> _logger;
|
||||||
|
|
||||||
|
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Richiesta non valida: {Message}", ex.Message);
|
||||||
|
await WriteErrorResponse(context, HttpStatusCode.BadRequest, ex.Message);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Operazione non valida: {Message}", ex.Message);
|
||||||
|
await WriteErrorResponse(context, HttpStatusCode.BadRequest, ex.Message);
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Risorsa non trovata: {Message}", ex.Message);
|
||||||
|
await WriteErrorResponse(context, HttpStatusCode.NotFound, "Risorsa non trovata.");
|
||||||
|
}
|
||||||
|
catch (TimeoutException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Timeout durante la generazione del report");
|
||||||
|
await WriteErrorResponse(context, HttpStatusCode.GatewayTimeout,
|
||||||
|
"Timeout nella generazione del report. Riprovare.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore non gestito: {Message}", ex.Message);
|
||||||
|
await WriteErrorResponse(context, HttpStatusCode.InternalServerError,
|
||||||
|
"Errore interno del server. Contattare l'assistenza.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task WriteErrorResponse(HttpContext context, HttpStatusCode statusCode, string message)
|
||||||
|
{
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
context.Response.StatusCode = (int)statusCode;
|
||||||
|
|
||||||
|
var errorResponse = new
|
||||||
|
{
|
||||||
|
status = (int)statusCode,
|
||||||
|
error = statusCode.ToString(),
|
||||||
|
message,
|
||||||
|
timestamp = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.Response.WriteAsync(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class GlobalExceptionMiddlewareExtensions
|
||||||
|
{
|
||||||
|
public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
return app.UseMiddleware<GlobalExceptionMiddleware>();
|
||||||
|
}
|
||||||
|
}
|
||||||
69
CertReports.Syncfusion/Helpers/HealthChecks.cs
Normal file
69
CertReports.Syncfusion/Helpers/HealthChecks.cs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica connettività al database SQL Server.
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
|
||||||
|
public DatabaseHealthCheck(IConfiguration config)
|
||||||
|
{
|
||||||
|
_connectionString = config.GetConnectionString("CertDb") ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync(cancellationToken);
|
||||||
|
await using var cmd = new SqlCommand("SELECT 1", conn);
|
||||||
|
await cmd.ExecuteScalarAsync(cancellationToken);
|
||||||
|
return HealthCheckResult.Healthy("SQL Server raggiungibile.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy("SQL Server non raggiungibile.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifica connettività al servizio chart esterno.
|
||||||
|
/// </summary>
|
||||||
|
public class ChartServiceHealthCheck : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
public ChartServiceHealthCheck(IHttpClientFactory httpClientFactory, IConfiguration config)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_baseUrl = config["ChartService:BaseUrl"] ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(
|
||||||
|
HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_baseUrl))
|
||||||
|
return HealthCheckResult.Degraded("ChartService:BaseUrl non configurato.");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var client = _httpClientFactory.CreateClient("ChartService");
|
||||||
|
var response = await client.GetAsync(_baseUrl, cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode
|
||||||
|
? HealthCheckResult.Healthy("Chart service raggiungibile.")
|
||||||
|
: HealthCheckResult.Degraded($"Chart service risponde con status {response.StatusCode}.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Degraded("Chart service non raggiungibile.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
126
CertReports.Syncfusion/Helpers/PdfTheme.cs
Normal file
126
CertReports.Syncfusion/Helpers/PdfTheme.cs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
using Syncfusion.Pdf.Grid;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tema centralizzato per tutti i PDF generati.
|
||||||
|
/// Modifica qui colori, font e dimensioni per aggiornare l'intero report.
|
||||||
|
/// </summary>
|
||||||
|
public static class PdfTheme
|
||||||
|
{
|
||||||
|
// ─── Colori ────────────────────────────────────────────────────────
|
||||||
|
public static readonly Color HeaderBackground = Color.FromArgb(255, 46, 80, 144); // #2E5090
|
||||||
|
public static readonly Color HeaderText = Color.FromArgb(255, 255, 255, 255);
|
||||||
|
public static readonly Color AlternateRow = Color.FromArgb(255, 242, 246, 250); // #F2F6FA
|
||||||
|
public static readonly Color BorderColor = Color.FromArgb(255, 200, 210, 220);
|
||||||
|
public static readonly Color TextPrimary = Color.FromArgb(255, 33, 37, 41);
|
||||||
|
public static readonly Color TextSecondary = Color.FromArgb(255, 108, 117, 125);
|
||||||
|
public static readonly Color PositiveValue = Color.FromArgb(255, 40, 167, 69); // verde
|
||||||
|
public static readonly Color NegativeValue = Color.FromArgb(255, 220, 53, 69); // rosso
|
||||||
|
public static readonly Color SectionTitle = Color.FromArgb(255, 46, 80, 144);
|
||||||
|
|
||||||
|
// ─── Font ──────────────────────────────────────────────────────────
|
||||||
|
private static readonly PdfStandardFont _fontRegular = new(PdfFontFamily.Helvetica, 8f);
|
||||||
|
private static readonly PdfStandardFont _fontBold = new(PdfFontFamily.Helvetica, 8f, PdfFontStyle.Bold);
|
||||||
|
private static readonly PdfStandardFont _fontSmall = new(PdfFontFamily.Helvetica, 6.5f);
|
||||||
|
private static readonly PdfStandardFont _fontSmallBold = new(PdfFontFamily.Helvetica, 6.5f, PdfFontStyle.Bold);
|
||||||
|
private static readonly PdfStandardFont _fontTitle = new(PdfFontFamily.Helvetica, 14f, PdfFontStyle.Bold);
|
||||||
|
private static readonly PdfStandardFont _fontSectionTitle = new(PdfFontFamily.Helvetica, 10f, PdfFontStyle.Bold);
|
||||||
|
private static readonly PdfStandardFont _fontHeader = new(PdfFontFamily.Helvetica, 7f, PdfFontStyle.Bold);
|
||||||
|
|
||||||
|
public static PdfFont Regular => _fontRegular;
|
||||||
|
public static PdfFont Bold => _fontBold;
|
||||||
|
public static PdfFont Small => _fontSmall;
|
||||||
|
public static PdfFont SmallBold => _fontSmallBold;
|
||||||
|
public static PdfFont Title => _fontTitle;
|
||||||
|
public static PdfFont SectionTitleFont => _fontSectionTitle;
|
||||||
|
public static PdfFont Header => _fontHeader;
|
||||||
|
|
||||||
|
// ─── Margini & Layout ──────────────────────────────────────────────
|
||||||
|
public const float PageMargin = 40f;
|
||||||
|
public const float RowHeight = 18f;
|
||||||
|
public const float HeaderRowHeight = 22f;
|
||||||
|
public const float CellPadding = 4f;
|
||||||
|
|
||||||
|
// ─── Brushes ───────────────────────────────────────────────────────
|
||||||
|
public static PdfBrush HeaderBrush => new PdfSolidBrush(HeaderBackground);
|
||||||
|
public static PdfBrush HeaderTextBrush => new PdfSolidBrush(HeaderText);
|
||||||
|
public static PdfBrush AlternateRowBrush => new PdfSolidBrush(AlternateRow);
|
||||||
|
public static PdfBrush TextBrush => new PdfSolidBrush(TextPrimary);
|
||||||
|
public static PdfBrush TextSecondaryBrush => new PdfSolidBrush(TextSecondary);
|
||||||
|
public static PdfBrush PositiveBrush => new PdfSolidBrush(PositiveValue);
|
||||||
|
public static PdfBrush NegativeBrush => new PdfSolidBrush(NegativeValue);
|
||||||
|
public static PdfPen BorderPen => new PdfPen(BorderColor, 0.5f);
|
||||||
|
|
||||||
|
// ─── Utility ───────────────────────────────────────────────────────
|
||||||
|
public static PdfBrush ValueBrush(decimal? value)
|
||||||
|
{
|
||||||
|
if (value == null) return TextBrush;
|
||||||
|
return value >= 0 ? PositiveBrush : NegativeBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Crea una pagina A4 con margini standard
|
||||||
|
/// </summary>
|
||||||
|
public static PdfPage AddA4Page(PdfDocument doc, PdfPageOrientation orientation = PdfPageOrientation.Portrait)
|
||||||
|
{
|
||||||
|
doc.PageSettings.Size = PdfPageSize.A4;
|
||||||
|
doc.PageSettings.Orientation = orientation;
|
||||||
|
doc.PageSettings.Margins.All = PageMargin;
|
||||||
|
return doc.Pages.Add();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formatta un decimale come percentuale
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatPercent(decimal? value, int decimals = 2)
|
||||||
|
{
|
||||||
|
if (value == null) return "-";
|
||||||
|
return value.Value.ToString($"N{decimals}") + " %";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formatta un decimale come numero
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatNumber(decimal? value, int decimals = 2)
|
||||||
|
{
|
||||||
|
if (value == null) return "-";
|
||||||
|
return value.Value.ToString($"N{decimals}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formatta una data in formato italiano
|
||||||
|
/// </summary>
|
||||||
|
public static string FormatDate(DateTime? date)
|
||||||
|
{
|
||||||
|
return date?.ToString("dd/MM/yyyy") ?? "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applica bordi sottili a tutte le celle di una PdfGrid
|
||||||
|
/// </summary>
|
||||||
|
public static void ApplyThinBorders(PdfGrid grid, float thickness = 0.25f)
|
||||||
|
{
|
||||||
|
var pen = new PdfPen(BorderColor, thickness);
|
||||||
|
var borders = new PdfBorders
|
||||||
|
{
|
||||||
|
Top = pen,
|
||||||
|
Bottom = pen,
|
||||||
|
Left = pen,
|
||||||
|
Right = pen
|
||||||
|
};
|
||||||
|
|
||||||
|
// Header
|
||||||
|
for (int r = 0; r < grid.Headers.Count; r++)
|
||||||
|
for (int c = 0; c < grid.Headers[r].Cells.Count; c++)
|
||||||
|
grid.Headers[r].Cells[c].Style.Borders = borders;
|
||||||
|
|
||||||
|
// Righe dati
|
||||||
|
for (int r = 0; r < grid.Rows.Count; r++)
|
||||||
|
for (int c = 0; c < grid.Rows[r].Cells.Count; c++)
|
||||||
|
grid.Rows[r].Cells[c].Style.Borders = borders;
|
||||||
|
}
|
||||||
|
}
|
||||||
158
CertReports.Syncfusion/Models/CertificateModels.cs
Normal file
158
CertReports.Syncfusion/Models/CertificateModels.cs
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
namespace CertReports.Syncfusion.Models;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Dati anagrafici del certificato (Sezione 1)
|
||||||
|
// SP: rpt_Master_CFT_ISIN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
public class CertificateInfo
|
||||||
|
{
|
||||||
|
// Identificazione
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
public string Emittente { get; set; } = string.Empty;
|
||||||
|
public string Nome { get; set; } = string.Empty;
|
||||||
|
public string Categoria { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Struttura
|
||||||
|
public string Mercato { get; set; } = string.Empty;
|
||||||
|
public string Valuta { get; set; } = string.Empty;
|
||||||
|
public string Direzione { get; set; } = string.Empty;
|
||||||
|
public string BasketType { get; set; } = string.Empty;
|
||||||
|
public string BarrierType { get; set; } = string.Empty;
|
||||||
|
public string Memory { get; set; } = string.Empty; // "SI"/"NO"/"N/A"
|
||||||
|
public string Airbag { get; set; } = string.Empty; // "SI"/"NO"/"N/A"
|
||||||
|
public string FrequenzaCedole { get; set; } = string.Empty; // "mensile","trimestrale",...
|
||||||
|
|
||||||
|
// Date (arrivano come stringhe formattate dalla SP)
|
||||||
|
public string Scadenza { get; set; } = string.Empty;
|
||||||
|
public string DataEmissione { get; set; } = string.Empty;
|
||||||
|
public string DataRimborso { get; set; } = string.Empty;
|
||||||
|
public string NextAutocallDate { get; set; } = string.Empty;
|
||||||
|
public string LastPriceDate { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Prezzi
|
||||||
|
public string Bid { get; set; } = string.Empty;
|
||||||
|
public string Ask { get; set; } = string.Empty;
|
||||||
|
public decimal? NominalValue { get; set; }
|
||||||
|
public decimal? PrezzoEmissione { get; set; }
|
||||||
|
|
||||||
|
// Rendimenti e protezioni (arrivano formattati dalla SP)
|
||||||
|
public string NominalAnnualYield { get; set; } = string.Empty;
|
||||||
|
public string CouponYield { get; set; } = string.Empty;
|
||||||
|
public string PotentialCouponYield { get; set; } = string.Empty;
|
||||||
|
public string MinimumYield { get; set; } = string.Empty;
|
||||||
|
public string CapitalReturnAtMaturity { get; set; } = string.Empty;
|
||||||
|
public string CapitalAnnualReturnAtMaturity { get; set; } = string.Empty;
|
||||||
|
public string AutocallReturn { get; set; } = string.Empty;
|
||||||
|
public string AutocallAnnualReturn { get; set; } = string.Empty;
|
||||||
|
public string TriggerAutocallDistance { get; set; } = string.Empty;
|
||||||
|
public string IRR { get; set; } = string.Empty;
|
||||||
|
public string RendimentoTotale { get; set; } = string.Empty;
|
||||||
|
public string RendimentoAttuale { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Valori calcolati (arrivano formattati)
|
||||||
|
public string IntrinsicValue { get; set; } = string.Empty;
|
||||||
|
public string Premium { get; set; } = string.Empty;
|
||||||
|
public string CapitalValue { get; set; } = string.Empty;
|
||||||
|
public string AutocallValue { get; set; } = string.Empty;
|
||||||
|
public string ValoreRimborso { get; set; } = string.Empty;
|
||||||
|
public string RTS { get; set; } = string.Empty;
|
||||||
|
public string Var95 { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Protezioni (arrivano formattate)
|
||||||
|
public string BufferKProt { get; set; } = string.Empty;
|
||||||
|
public string BufferCPNProt { get; set; } = string.Empty;
|
||||||
|
public string LivelloBarriera { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
// Cedole
|
||||||
|
public decimal? CpnPagati { get; set; }
|
||||||
|
public decimal? CpnDaPagare { get; set; }
|
||||||
|
public decimal? CpnInMemoria { get; set; }
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Sottostanti (da SP separata: rpt_Details_UL_ISIN)
|
||||||
|
public List<Sottostante> Sottostanti { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Sottostanti del certificato
|
||||||
|
// SP: rpt_Details_UL_ISIN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
public class Sottostante
|
||||||
|
{
|
||||||
|
public string Nome { get; set; } = string.Empty;
|
||||||
|
public string Strike { get; set; } = string.Empty;
|
||||||
|
public string LastPrice { get; set; } = string.Empty;
|
||||||
|
public string Performance { get; set; } = string.Empty;
|
||||||
|
public string CapitalBarrier { get; set; } = string.Empty;
|
||||||
|
public string CouponBarrier { get; set; } = string.Empty;
|
||||||
|
public string TriggerAutocall { get; set; } = string.Empty;
|
||||||
|
public string ULCapitalBarrierBuffer { get; set; } = string.Empty;
|
||||||
|
public string ULCouponBarrierBuffer { get; set; } = string.Empty;
|
||||||
|
public string ULTriggerAutocallDistance { get; set; } = string.Empty;
|
||||||
|
public string DividendExDate { get; set; } = string.Empty;
|
||||||
|
public string DividendPayDate { get; set; } = string.Empty;
|
||||||
|
public string DividendAmount { get; set; } = string.Empty;
|
||||||
|
public string DividendYield { get; set; } = string.Empty;
|
||||||
|
public string DividendFutAmount { get; set; } = string.Empty;
|
||||||
|
public string DividendFutYield { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Eventi del certificato (Sezione 2)
|
||||||
|
// SP: rpt_Events_CFT_ISIN
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
public class CertificateEvent
|
||||||
|
{
|
||||||
|
public string ObservationDate { get; set; } = string.Empty;
|
||||||
|
public string ExDate { get; set; } = string.Empty;
|
||||||
|
public string RecordDate { get; set; } = string.Empty;
|
||||||
|
public string PaymentDate { get; set; } = string.Empty;
|
||||||
|
public string CouponTrigger { get; set; } = string.Empty;
|
||||||
|
public string CouponValue { get; set; } = string.Empty;
|
||||||
|
public string Paid { get; set; } = string.Empty;
|
||||||
|
public string Memory { get; set; } = string.Empty;
|
||||||
|
public string AmountPaid { get; set; } = string.Empty;
|
||||||
|
public string CapitalTrigger { get; set; } = string.Empty;
|
||||||
|
public string CapitalValue { get; set; } = string.Empty;
|
||||||
|
public string AutocallTrigger { get; set; } = string.Empty;
|
||||||
|
public string AutocallValue { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Analisi scenario / Analisi rischio (Sezione 3)
|
||||||
|
// SP: rpt_AnalisiRischio_ISIN
|
||||||
|
// Restituisce righe pivot con col0..col11 (tutte varchar)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
public class ScenarioRow
|
||||||
|
{
|
||||||
|
/// <summary>col0: "Prezzo Sottostante", "Rimborso Certificato", "P&L % certificato"</summary>
|
||||||
|
public string Label { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>col1..col11: valori per variazione -70%, -50%, -30%, -20%, -10%, 0%, +10%, +20%, +30%, +50%, +70%</summary>
|
||||||
|
public List<string> Values { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ScenarioAnalysis
|
||||||
|
{
|
||||||
|
public static readonly string[] VariationHeaders =
|
||||||
|
{ "-90%", "-80%", "-70%", "-60%", "-50%", "-40%", "-30%", "-20%", "-10%", "0%", "+10%", "+20%", "+30%" };
|
||||||
|
|
||||||
|
public List<ScenarioRow> Rows { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Modello aggregato per generazione report
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
public class CertificateReportData
|
||||||
|
{
|
||||||
|
public CertificateInfo Info { get; set; } = new();
|
||||||
|
public List<CertificateEvent> Eventi { get; set; } = new();
|
||||||
|
public ScenarioAnalysis Scenario { get; set; } = new();
|
||||||
|
public byte[]? ChartImage { get; set; }
|
||||||
|
}
|
||||||
53
CertReports.Syncfusion/Models/ChartModels.cs
Normal file
53
CertReports.Syncfusion/Models/ChartModels.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
namespace CertReports.Syncfusion.Models;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Dati per la generazione del grafico (Sezione 4)
|
||||||
|
// SP: FSWeb_Chart_UL, FSWeb_Chart_DailyCTF, FSWeb_Chart_DailyUL
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Informazioni sui sottostanti per il grafico.
|
||||||
|
/// Da SP: FSWeb_Chart_UL
|
||||||
|
/// </summary>
|
||||||
|
public class ChartUnderlyingInfo
|
||||||
|
{
|
||||||
|
public int IDCertificates { get; set; }
|
||||||
|
public int IDUnderlyings { get; set; }
|
||||||
|
public DateTime StartDate { get; set; }
|
||||||
|
public decimal Strike { get; set; }
|
||||||
|
public decimal BarrieraCoupon { get; set; }
|
||||||
|
public decimal BarrieraCapitale { get; set; }
|
||||||
|
public string Sottostante { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Singolo punto dati di una serie temporale (data + performance %).
|
||||||
|
/// Da SP: FSWeb_Chart_DailyCTF e FSWeb_Chart_DailyUL
|
||||||
|
/// </summary>
|
||||||
|
public class ChartDataPoint
|
||||||
|
{
|
||||||
|
public DateTime Date { get; set; }
|
||||||
|
public decimal Performance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Una serie completa da disegnare nel grafico.
|
||||||
|
/// </summary>
|
||||||
|
public class ChartSeries
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public List<ChartDataPoint> Points { get; set; } = new();
|
||||||
|
public bool IsCertificate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tutti i dati necessari per disegnare il grafico.
|
||||||
|
/// </summary>
|
||||||
|
public class ChartRenderData
|
||||||
|
{
|
||||||
|
public string Isin { get; set; } = string.Empty;
|
||||||
|
public List<ChartSeries> Series { get; set; } = new();
|
||||||
|
public decimal BarrieraCapitale { get; set; }
|
||||||
|
public decimal BarrieraCoupon { get; set; }
|
||||||
|
public decimal Strike { get; set; } = 100;
|
||||||
|
}
|
||||||
62
CertReports.Syncfusion/Program.cs
Normal file
62
CertReports.Syncfusion/Program.cs
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Services.Implementations;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// ── Serilog ────────────────────────────────────────────────────────────
|
||||||
|
Log.Logger = new LoggerConfiguration()
|
||||||
|
.ReadFrom.Configuration(builder.Configuration)
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.WriteTo.Console()
|
||||||
|
.WriteTo.File("Logs/certreports-.log", rollingInterval: RollingInterval.Day)
|
||||||
|
.CreateLogger();
|
||||||
|
builder.Host.UseSerilog();
|
||||||
|
|
||||||
|
// ── Syncfusion Community License ───────────────────────────────────────
|
||||||
|
Syncfusion.Licensing.SyncfusionLicenseProvider
|
||||||
|
.RegisterLicense(builder.Configuration["Syncfusion:LicenseKey"] ?? "");
|
||||||
|
|
||||||
|
// ── Services (DI) ──────────────────────────────────────────────────────
|
||||||
|
builder.Services.AddControllers();
|
||||||
|
|
||||||
|
// Memory cache per i report PDF generati
|
||||||
|
builder.Services.AddMemoryCache(options =>
|
||||||
|
{
|
||||||
|
options.SizeLimit = 200 * 1024 * 1024; // 200 MB max
|
||||||
|
});
|
||||||
|
|
||||||
|
// HttpClient factory per chiamate al chart service
|
||||||
|
builder.Services.AddHttpClient("ChartService", client =>
|
||||||
|
{
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Health checks
|
||||||
|
builder.Services.AddHealthChecks()
|
||||||
|
.AddCheck<DatabaseHealthCheck>("database", tags: new[] { "db", "ready" })
|
||||||
|
.AddCheck<ChartServiceHealthCheck>("chart-service", tags: new[] { "external", "ready" });
|
||||||
|
|
||||||
|
// Registra i servizi applicativi
|
||||||
|
builder.Services.AddScoped<ICertificateDataService, CertificateDataService>();
|
||||||
|
builder.Services.AddScoped<IChartDataService, ChartDataService>();
|
||||||
|
builder.Services.AddScoped<IPdfSectionRenderer, AnagraficaSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<IPdfSectionRenderer, EventiSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<IPdfSectionRenderer, ScenarioSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<IChartSectionRenderer, ChartSectionRenderer>();
|
||||||
|
builder.Services.AddScoped<IPdfMergerService, PdfMergerService>();
|
||||||
|
builder.Services.AddScoped<IReportOrchestrator, ReportOrchestrator>();
|
||||||
|
builder.Services.AddSingleton<IPdfCacheService, PdfCacheService>();
|
||||||
|
builder.Services.AddSingleton<CryptoHelper>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
// ── Middleware Pipeline ────────────────────────────────────────────────
|
||||||
|
app.UseGlobalExceptionHandler(); // Gestione errori centralizzata (primo!)
|
||||||
|
app.UseSerilogRequestLogging();
|
||||||
|
app.UseRouting();
|
||||||
|
app.MapControllers();
|
||||||
|
app.MapHealthChecks("/health"); // GET /health per monitoring
|
||||||
|
|
||||||
|
app.Run();
|
||||||
12
CertReports.Syncfusion/Properties/launchSettings.json
Normal file
12
CertReports.Syncfusion/Properties/launchSettings.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"CertReports.Syncfusion": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
},
|
||||||
|
"applicationUrl": "https://localhost:51308;http://localhost:51309"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
164
CertReports.Syncfusion/README.md
Normal file
164
CertReports.Syncfusion/README.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# CertReports.Syncfusion
|
||||||
|
|
||||||
|
Progetto ASP.NET Core 8 per la generazione di report PDF per certificati finanziari,
|
||||||
|
basato su **Syncfusion PDF Library** (Community License).
|
||||||
|
|
||||||
|
Sostituisce il vecchio progetto WebForms con DevExpress + PdfSharp.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architettura
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ReportController │
|
||||||
|
│ GET /api/report?p={encrypted} | GET /api/report?alias={id} │
|
||||||
|
└──────────────────────┬───────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ ReportOrchestrator │
|
||||||
|
│ │
|
||||||
|
│ 1. CertificateDataService ──→ SQL Server (stored procedures) │
|
||||||
|
│ 2. AnagraficaSectionRenderer ──→ PDF Sezione 1 │
|
||||||
|
│ 3. EventiSectionRenderer ──→ PDF Sezione 2 │
|
||||||
|
│ 4. ScenarioSectionRenderer ──→ PDF Sezione 3 (se non Protect.) │
|
||||||
|
│ 5. ChartSectionRenderer ──→ PDF Sezione 4 (ext. service) │
|
||||||
|
│ 6. PdfMergerService ──→ PDF Finale unificato │
|
||||||
|
└──────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Struttura file
|
||||||
|
|
||||||
|
```
|
||||||
|
CertReports.Syncfusion/
|
||||||
|
├── Controllers/
|
||||||
|
│ └── ReportController.cs # API endpoint (sostituisce WebForm)
|
||||||
|
├── Helpers/
|
||||||
|
│ ├── CryptoHelper.cs # Decodifica ISIN cifrato
|
||||||
|
│ └── PdfTheme.cs # Colori, font, stili centralizzati
|
||||||
|
├── Models/
|
||||||
|
│ └── CertificateModels.cs # Modelli dati (Info, Eventi, Scenario)
|
||||||
|
├── Services/
|
||||||
|
│ ├── Interfaces/
|
||||||
|
│ │ └── IServices.cs # Contratti per tutti i servizi
|
||||||
|
│ └── Implementations/
|
||||||
|
│ ├── CertificateDataService.cs # Accesso DB via stored procedures
|
||||||
|
│ ├── AnagraficaSectionRenderer.cs # Sezione 1: dati + sottostanti
|
||||||
|
│ ├── EventiSectionRenderer.cs # Sezione 2: tabella eventi
|
||||||
|
│ ├── ScenarioSectionRenderer.cs # Sezione 3: matrice scenario
|
||||||
|
│ ├── ChartSectionRenderer.cs # Sezione 4: grafico
|
||||||
|
│ ├── PdfMergerService.cs # Merge PDF Syncfusion
|
||||||
|
│ └── ReportOrchestrator.cs # Coordinatore principale
|
||||||
|
├── GlobalUsings.cs
|
||||||
|
├── Program.cs # Entry point + DI setup
|
||||||
|
├── appsettings.json # Configurazione
|
||||||
|
└── CertReports.Syncfusion.csproj # Progetto + NuGet packages
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mapping Vecchio → Nuovo
|
||||||
|
|
||||||
|
| Vecchio (DevExpress/WebForms) | Nuovo (Syncfusion/ASP.NET Core) |
|
||||||
|
|----------------------------------------------|-------------------------------------------|
|
||||||
|
| `ReportFSSiteCrypt.aspx` (Page_Load) | `ReportController.cs` (API REST) |
|
||||||
|
| `XtraReport` + `.repx` template | `IPdfSectionRenderer` implementations |
|
||||||
|
| `report.ExportToPdf()` per ogni sezione | Ogni renderer restituisce `PdfDocument` |
|
||||||
|
| `PdfSharp.PdfReader.Open` + `CopyPages` | `Syncfusion PdfMergerService` |
|
||||||
|
| `CommonClass.DecryptCombined()` | `CryptoHelper.DecryptIsin()` |
|
||||||
|
| `CommonClass.execSP_Scalar()` | `CertificateDataService` (async) |
|
||||||
|
| `CallWebApi()` per chart | `ChartSectionRenderer` (HttpClientFactory)|
|
||||||
|
| File temp su disco + `File.Delete` | Tutto in memoria (MemoryStream) |
|
||||||
|
| `Response.BinaryWrite()` | `return File(bytes, "application/pdf")` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup e Configurazione
|
||||||
|
|
||||||
|
### 1. Prerequisiti
|
||||||
|
- .NET 8 SDK
|
||||||
|
- SQL Server con le stored procedures esistenti
|
||||||
|
- Syncfusion Community License Key (gratuita per < $1M fatturato)
|
||||||
|
|
||||||
|
### 2. Configurazione `appsettings.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"CertDb": "Server=...;Database=...;User Id=...;Password=...;"
|
||||||
|
},
|
||||||
|
"Syncfusion": {
|
||||||
|
"LicenseKey": "LA_TUA_CHIAVE_QUI"
|
||||||
|
},
|
||||||
|
"ChartService": {
|
||||||
|
"BaseUrl": "https://reports.smart-roots.net:4004",
|
||||||
|
"ChartEndpoint": "/ChartFSWeb.aspx?width=800&height=600&isin={0}&tipo=pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Avvio
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dotnet restore
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test
|
||||||
|
|
||||||
|
```
|
||||||
|
# Con ISIN cifrato (compatibile vecchie URL)
|
||||||
|
GET https://localhost:5001/api/report?p=0x0200000047...
|
||||||
|
|
||||||
|
# Con Alias ID
|
||||||
|
GET https://localhost:5001/api/report?alias=MY_ALIAS
|
||||||
|
|
||||||
|
# Diretto (debug/interno)
|
||||||
|
GET https://localhost:5001/api/report/by-isin/CH1277653163
|
||||||
|
|
||||||
|
# Download come file
|
||||||
|
GET https://localhost:5001/api/report/download?alias=MY_ALIAS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stored Procedures Utilizzate
|
||||||
|
|
||||||
|
| SP | Sezione Report | Descrizione |
|
||||||
|
|----|----------------|-------------|
|
||||||
|
| `rpt_Master_CFT_ISIN(@ISIN)` | 1 - Anagrafica | Dati certificato (1 record, campi pre-formattati) |
|
||||||
|
| `rpt_Details_UL_ISIN(@ISIN)` | 1 - Sottostanti | N sottostanti con strike, barriere, dividendi |
|
||||||
|
| `rpt_Events_CFT_ISIN(@ISIN)` | 2 - Eventi | Lista eventi con date, cedole, autocall |
|
||||||
|
| `rpt_AnalisiRischio_ISIN(@ISIN)` | 3 - Scenario | Tabella pivot 3×11 (variazioni -70%..+70%) |
|
||||||
|
| `rpt_FindIsinbyAliasID(@AliasID)` | Risoluzione | Alias ID → ISIN |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## TODO Rimasti
|
||||||
|
|
||||||
|
### Completato ✓
|
||||||
|
- ✓ CryptoHelper con logica `DecryptCombined` completa (v1 TripleDES, v2 AES)
|
||||||
|
- ✓ Mapping colonne SP reali (`rpt_Master_CFT_ISIN`, `rpt_Details_UL_ISIN`, ecc.)
|
||||||
|
- ✓ Modelli aggiornati per ricevere stringhe pre-formattate dalla SP
|
||||||
|
- ✓ Analisi scenario adattata al formato pivot `col0..col11`
|
||||||
|
|
||||||
|
### Da Verificare/Completare
|
||||||
|
1. **Test con DB reale** — Verificare che i nomi colonna nel `SqlDataReader` corrispondano
|
||||||
|
esattamente all'output delle SP (usa `GetStringSafe` che gestisce colonne mancanti)
|
||||||
|
2. **Layout PDF fine-tuning** — Posizioni, margini, colori per replicare il look originale
|
||||||
|
3. **Chart service** — L'endpoint ChartFSWeb.aspx funziona ancora? Altrimenti
|
||||||
|
implementare la generazione interna con SkiaSharp in `ChartSectionRenderer`
|
||||||
|
4. **SP `rpt_FindIsinbyAliasID`** — Verificare nome esatto e parametro
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vantaggi rispetto al Vecchio Progetto
|
||||||
|
|
||||||
|
- **Nessun file temporaneo su disco**: tutto in MemoryStream
|
||||||
|
- **Dependency Injection**: ogni servizio è testabile e sostituibile
|
||||||
|
- **Async/await**: niente più `WebRequest` bloccanti
|
||||||
|
- **Logging strutturato**: Serilog con log rotanti
|
||||||
|
- **Sezioni modulari**: aggiungere un nuovo tipo di report = aggiungere un `IPdfSectionRenderer`
|
||||||
|
- **Tema centralizzato**: un cambio in `PdfTheme.cs` aggiorna tutti i PDF
|
||||||
|
- **API REST**: può essere consumata da qualsiasi client (non solo browser)
|
||||||
@@ -0,0 +1,256 @@
|
|||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
using Syncfusion.Pdf.Grid;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sezione 1: Dati anagrafici del certificato + sottostanti.
|
||||||
|
/// Dati da: rpt_Master_CFT_ISIN + rpt_Details_UL_ISIN
|
||||||
|
/// </summary>
|
||||||
|
public class AnagraficaSectionRenderer : IPdfSectionRenderer
|
||||||
|
{
|
||||||
|
public string SectionName => "Anagrafica";
|
||||||
|
public int Order => 1;
|
||||||
|
|
||||||
|
public PdfDocument Render(CertificateReportData data)
|
||||||
|
{
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
var page = PdfTheme.AddA4Page(doc);
|
||||||
|
var g = page.Graphics;
|
||||||
|
float y = 0;
|
||||||
|
float w = page.GetClientSize().Width;
|
||||||
|
var info = data.Info;
|
||||||
|
|
||||||
|
// ── Titolo ─────────────────────────────────────────────────────
|
||||||
|
g.DrawString($"Scheda Prodotto {info.Isin}", PdfTheme.Title, PdfTheme.TextBrush,
|
||||||
|
new RectangleF(0, y, w, 25));
|
||||||
|
y += 28;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(info.Categoria))
|
||||||
|
{
|
||||||
|
g.DrawString($"Tipologia: {info.Categoria}", PdfTheme.SectionTitleFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.SectionTitle), new RectangleF(0, y, w, 18));
|
||||||
|
y += 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Due colonne: Caratteristiche (sx) + Info emittente (dx) ────
|
||||||
|
float colW = (w - 20) / 2;
|
||||||
|
|
||||||
|
// Colonna sinistra
|
||||||
|
float ly = DrawSectionHeader(g, "Caratteristiche Prodotto", 0, colW, y);
|
||||||
|
ly = DrawKV(g, 0, colW, ly, new()
|
||||||
|
{
|
||||||
|
["Cedola Annua"] = info.NominalAnnualYield,
|
||||||
|
["Valore Nominale"] = info.NominalValue?.ToString("F0") ?? "-",
|
||||||
|
["Prezzo Emissione"] = info.PrezzoEmissione?.ToString("F0") ?? "-",
|
||||||
|
["Memoria"] = info.Memory,
|
||||||
|
["Frequenza Cedole"] = info.FrequenzaCedole,
|
||||||
|
["Tipo Barriera"] = info.BarrierType,
|
||||||
|
["Tipo Basket"] = info.BasketType,
|
||||||
|
["Livello Barriera"] = info.LivelloBarriera,
|
||||||
|
["Direzione"] = info.Direzione,
|
||||||
|
["Airbag"] = info.Airbag,
|
||||||
|
["Leva"] = info.Leva,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Colonna destra
|
||||||
|
float rx = colW + 20;
|
||||||
|
float ry = DrawSectionHeader(g, "Informazioni", rx, colW, y);
|
||||||
|
ry = DrawKV(g, rx, colW, ry, new()
|
||||||
|
{
|
||||||
|
["Emittente"] = info.Emittente,
|
||||||
|
["ISIN"] = info.Isin,
|
||||||
|
["Mercato"] = info.Mercato,
|
||||||
|
["Valuta"] = info.Valuta,
|
||||||
|
["Data Emissione"] = info.DataEmissione,
|
||||||
|
["Data Scadenza"] = info.Scadenza,
|
||||||
|
["Prossimo Autocall"] = info.NextAutocallDate,
|
||||||
|
["Data Rimborso"] = info.DataRimborso,
|
||||||
|
["Nome"] = info.Nome,
|
||||||
|
});
|
||||||
|
|
||||||
|
y = Math.Max(ly, ry) + 12;
|
||||||
|
|
||||||
|
// ── Prezzi ─────────────────────────────────────────────────────
|
||||||
|
ly = DrawSectionHeader(g, "Prezzi", 0, colW, y);
|
||||||
|
ly = DrawKV(g, 0, colW, ly, new()
|
||||||
|
{
|
||||||
|
["Bid"] = info.Bid,
|
||||||
|
["Ask"] = info.Ask,
|
||||||
|
["Data/Ora Prezzo"] = info.LastPriceDate,
|
||||||
|
["Valore Intrinseco"] = info.IntrinsicValue,
|
||||||
|
["Premio"] = info.Premium,
|
||||||
|
["RTS"] = info.RTS,
|
||||||
|
["VaR 95%"] = info.Var95,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Rendimenti ─────────────────────────────────────────────────
|
||||||
|
ry = DrawSectionHeader(g, "Rendimenti e Protezioni", rx, colW, y);
|
||||||
|
ry = DrawKV(g, rx, colW, ry, new()
|
||||||
|
{
|
||||||
|
["Rend. Capitale a Scadenza"] = info.CapitalReturnAtMaturity,
|
||||||
|
["Rend. Annuo Capitale Scad."] = info.CapitalAnnualReturnAtMaturity,
|
||||||
|
["Rend. Autocall"] = info.AutocallReturn,
|
||||||
|
["Rend. Annuo Autocall"] = info.AutocallAnnualReturn,
|
||||||
|
["Distanza Autocall"] = info.TriggerAutocallDistance,
|
||||||
|
["IRR"] = info.IRR,
|
||||||
|
["Rendimento Totale"] = info.RendimentoTotale,
|
||||||
|
["Rendimento Attuale"] = info.RendimentoAttuale,
|
||||||
|
["Protezione Capitale"] = info.BufferKProt,
|
||||||
|
["Protezione Coupon"] = info.BufferCPNProt,
|
||||||
|
});
|
||||||
|
|
||||||
|
y = Math.Max(ly, ry) + 12;
|
||||||
|
|
||||||
|
// ── Cedole e Valori ────────────────────────────────────────────
|
||||||
|
ly = DrawSectionHeader(g, "Cedole", 0, colW, y);
|
||||||
|
ly = DrawKV(g, 0, colW, ly, new()
|
||||||
|
{
|
||||||
|
["Cedola Annua"] = info.NominalAnnualYield,
|
||||||
|
["Coupon Yield"] = info.CouponYield,
|
||||||
|
["Potential Coupon Yield"] = info.PotentialCouponYield,
|
||||||
|
["Minimum Yield"] = info.MinimumYield,
|
||||||
|
["Cedole Pagate"] = info.CpnPagati?.ToString("F2") ?? "-",
|
||||||
|
["Cedole da Pagare"] = info.CpnDaPagare?.ToString("F2") ?? "-",
|
||||||
|
["Cedole in Memoria"] = info.CpnInMemoria?.ToString("F2") ?? "-",
|
||||||
|
});
|
||||||
|
|
||||||
|
ry = DrawSectionHeader(g, "Valori", rx, colW, y);
|
||||||
|
ry = DrawKV(g, rx, colW, ry, new()
|
||||||
|
{
|
||||||
|
["Valore Capitale"] = info.CapitalValue,
|
||||||
|
["Valore Autocall"] = info.AutocallValue,
|
||||||
|
["Valore Rimborso"] = info.ValoreRimborso,
|
||||||
|
["Fattore Airbag"] = info.FattoreAirbag,
|
||||||
|
["Trigger OneStar"] = info.TriggerOneStar,
|
||||||
|
});
|
||||||
|
|
||||||
|
y = Math.Max(ly, ry) + 15;
|
||||||
|
|
||||||
|
// ── Tabella Sottostanti ────────────────────────────────────────
|
||||||
|
if (info.Sottostanti.Count > 0)
|
||||||
|
{
|
||||||
|
// Se non c'è spazio nella pagina corrente, aggiungi nuova pagina
|
||||||
|
if (y > page.GetClientSize().Height - 150)
|
||||||
|
{
|
||||||
|
page = doc.Pages.Add();
|
||||||
|
g = page.Graphics;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
y = DrawSottostantiTable(g, info.Sottostanti, w, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Sottostanti
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawSottostantiTable(PdfGraphics g, List<Sottostante> sottostanti, float width, float y)
|
||||||
|
{
|
||||||
|
y = DrawSectionHeader(g, "Analisi Sottostanti", 0, width, y);
|
||||||
|
|
||||||
|
var grid = new PdfGrid();
|
||||||
|
grid.Style.CellPadding = new PdfPaddings(3, 3, 2, 2);
|
||||||
|
|
||||||
|
string[] headers = { "Nome", "Strike", "Last", "% Perf.", "Barr.K", "Buffer K",
|
||||||
|
"Barr.CPN", "Buffer CPN", "Trigger AC", "Dist.AC" };
|
||||||
|
|
||||||
|
foreach (var _ in headers) grid.Columns.Add();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
var hr = grid.Headers.Add(1)[0];
|
||||||
|
for (int i = 0; i < headers.Length; i++)
|
||||||
|
{
|
||||||
|
hr.Cells[i].Value = headers[i];
|
||||||
|
hr.Cells[i].Style.Font = PdfTheme.Header;
|
||||||
|
hr.Cells[i].Style.BackgroundBrush = PdfTheme.HeaderBrush;
|
||||||
|
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush as PdfBrush;
|
||||||
|
hr.Cells[i].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
|
||||||
|
}
|
||||||
|
hr.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Left);
|
||||||
|
|
||||||
|
// Righe
|
||||||
|
for (int i = 0; i < sottostanti.Count; i++)
|
||||||
|
{
|
||||||
|
var s = sottostanti[i];
|
||||||
|
var row = grid.Rows.Add();
|
||||||
|
row.Cells[0].Value = s.Nome;
|
||||||
|
row.Cells[1].Value = s.Strike;
|
||||||
|
row.Cells[2].Value = s.LastPrice;
|
||||||
|
row.Cells[3].Value = s.Performance;
|
||||||
|
row.Cells[4].Value = s.CapitalBarrier;
|
||||||
|
row.Cells[5].Value = s.ULCapitalBarrierBuffer;
|
||||||
|
row.Cells[6].Value = s.CouponBarrier;
|
||||||
|
row.Cells[7].Value = s.ULCouponBarrierBuffer;
|
||||||
|
row.Cells[8].Value = s.TriggerAutocall;
|
||||||
|
row.Cells[9].Value = s.ULTriggerAutocallDistance;
|
||||||
|
|
||||||
|
foreach (var cell in row.Cells.OfType<PdfGridCell>())
|
||||||
|
{
|
||||||
|
cell.Style.Font = PdfTheme.Small;
|
||||||
|
cell.StringFormat = new PdfStringFormat(PdfTextAlignment.Right);
|
||||||
|
}
|
||||||
|
row.Cells[0].StringFormat = new PdfStringFormat(PdfTextAlignment.Left);
|
||||||
|
|
||||||
|
if (i % 2 == 1)
|
||||||
|
foreach (var cell in row.Cells.OfType<PdfGridCell>())
|
||||||
|
cell.Style.BackgroundBrush = PdfTheme.AlternateRowBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Larghezze
|
||||||
|
float[] cw = { 80, 52, 52, 48, 52, 48, 52, 48, 52, 48 };
|
||||||
|
float total = cw.Sum();
|
||||||
|
float scale = width / total;
|
||||||
|
for (int i = 0; i < cw.Length; i++)
|
||||||
|
grid.Columns[i].Width = cw[i] * scale;
|
||||||
|
|
||||||
|
PdfTheme.ApplyThinBorders(grid);
|
||||||
|
|
||||||
|
grid.Draw(g, new PointF(0, y));
|
||||||
|
return y + (grid.Rows.Count + 1) * PdfTheme.RowHeight + 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Helper di disegno
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private float DrawSectionHeader(PdfGraphics g, string title, float x, float width, float y)
|
||||||
|
{
|
||||||
|
g.DrawRectangle(PdfTheme.HeaderBrush, new RectangleF(x, y, width, PdfTheme.HeaderRowHeight));
|
||||||
|
g.DrawString(title, PdfTheme.Header, PdfTheme.HeaderTextBrush,
|
||||||
|
new RectangleF(x + PdfTheme.CellPadding, y + 3, width, PdfTheme.HeaderRowHeight));
|
||||||
|
return y + PdfTheme.HeaderRowHeight + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float DrawKV(PdfGraphics g, float x, float width, float y, Dictionary<string, string> data)
|
||||||
|
{
|
||||||
|
float labelW = width * 0.55f;
|
||||||
|
float valueW = width * 0.45f;
|
||||||
|
|
||||||
|
foreach (var kvp in data)
|
||||||
|
{
|
||||||
|
// Salta campi vuoti per compattare il layout
|
||||||
|
if (string.IsNullOrWhiteSpace(kvp.Value) || kvp.Value == "-") continue;
|
||||||
|
|
||||||
|
g.DrawString(kvp.Key, PdfTheme.Small, PdfTheme.TextSecondaryBrush,
|
||||||
|
new RectangleF(x + PdfTheme.CellPadding, y, labelW, PdfTheme.RowHeight));
|
||||||
|
g.DrawString(kvp.Value, PdfTheme.SmallBold, PdfTheme.TextBrush,
|
||||||
|
new RectangleF(x + labelW, y, valueW - PdfTheme.CellPadding, PdfTheme.RowHeight),
|
||||||
|
new PdfStringFormat(PdfTextAlignment.Right));
|
||||||
|
|
||||||
|
g.DrawLine(new PdfPen(PdfTheme.BorderColor, 0.3f),
|
||||||
|
x, y + PdfTheme.RowHeight, x + width, y + PdfTheme.RowHeight);
|
||||||
|
|
||||||
|
y += PdfTheme.RowHeight;
|
||||||
|
}
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Accesso dati SQL Server tramite le stored procedures reali del database FirstSolutionDB.
|
||||||
|
///
|
||||||
|
/// SP utilizzate:
|
||||||
|
/// - rpt_Master_CFT_ISIN: Anagrafica certificato (un singolo record)
|
||||||
|
/// - rpt_Details_UL_ISIN: Sottostanti del certificato (N record)
|
||||||
|
/// - rpt_Events_CFT_ISIN: Eventi del certificato (N record)
|
||||||
|
/// - rpt_AnalisiRischio_ISIN: Analisi scenario pivot (3 righe × 11 colonne)
|
||||||
|
/// - rpt_FindIsinbyAliasID: Risoluzione alias → ISIN
|
||||||
|
///
|
||||||
|
/// NOTA: Le SP restituiscono la maggior parte dei valori già formattati come stringhe
|
||||||
|
/// (FORMAT(...,'P2'), FORMAT(...,'F2'), etc.). I modelli usano string per questi campi
|
||||||
|
/// per evitare parsing/riformattazione non necessaria.
|
||||||
|
/// </summary>
|
||||||
|
public class CertificateDataService : ICertificateDataService
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
private readonly ILogger<CertificateDataService> _logger;
|
||||||
|
|
||||||
|
public CertificateDataService(IConfiguration config, ILogger<CertificateDataService> logger)
|
||||||
|
{
|
||||||
|
_connectionString = config.GetConnectionString("CertDb")
|
||||||
|
?? throw new InvalidOperationException("ConnectionString 'CertDb' non configurata.");
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Anagrafica + Sottostanti ──────────────────────────────────────
|
||||||
|
|
||||||
|
public async Task<CertificateInfo> GetCertificateInfoAsync(string isin)
|
||||||
|
{
|
||||||
|
var info = new CertificateInfo { Isin = isin };
|
||||||
|
|
||||||
|
await using var conn = new SqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
// ── 1. Dati anagrafici (rpt_Master_CFT_ISIN) ──────────────────
|
||||||
|
await using (var cmd = new SqlCommand("rpt_Master_CFT_ISIN", conn) { CommandType = CommandType.StoredProcedure })
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@ISIN", isin);
|
||||||
|
|
||||||
|
await using var r = await cmd.ExecuteReaderAsync();
|
||||||
|
if (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
info.Id = r.GetSafe<int>("ID");
|
||||||
|
info.Emittente = r.GetStringSafe("Emittente");
|
||||||
|
info.Nome = r.GetStringSafe("Nome");
|
||||||
|
info.Categoria = r.GetStringSafe("Categoria");
|
||||||
|
info.Scadenza = r.GetStringSafe("Scadenza");
|
||||||
|
info.Valuta = r.GetStringSafe("Valuta");
|
||||||
|
info.Mercato = r.GetStringSafe("Mercato");
|
||||||
|
info.Direzione = r.GetStringSafe("Direzione");
|
||||||
|
info.BasketType = r.GetStringSafe("BasketType");
|
||||||
|
info.BarrierType = r.GetStringSafe("BarrierType");
|
||||||
|
info.LastPriceDate = r.GetStringSafe("LastPriceDate");
|
||||||
|
info.Ask = r.GetStringSafe("Ask");
|
||||||
|
info.Bid = r.GetStringSafe("Bid");
|
||||||
|
info.NextAutocallDate = r.GetStringSafe("NextAutocallDate");
|
||||||
|
info.TriggerAutocallDistance = r.GetStringSafe("TriggerAutocallDistance");
|
||||||
|
info.AutocallReturn = r.GetStringSafe("AutocallReturn");
|
||||||
|
info.AutocallAnnualReturn = r.GetStringSafe("AutocallAnnualReturn");
|
||||||
|
info.CapitalReturnAtMaturity = r.GetStringSafe("CapitalReturnAtMaturity");
|
||||||
|
info.CapitalAnnualReturnAtMaturity = r.GetStringSafe("CapitalAnnualReturnAtMaturity");
|
||||||
|
info.NominalAnnualYield = r.GetStringSafe("NominalAnnualYield");
|
||||||
|
info.CouponYield = r.GetStringSafe("CouponYield");
|
||||||
|
info.PotentialCouponYield = r.GetStringSafe("PotentialCouponYield");
|
||||||
|
info.MinimumYield = r.GetStringSafe("MinimumYield");
|
||||||
|
info.IntrinsicValue = r.GetStringSafe("IntrinsicValue");
|
||||||
|
info.NominalValue = r.GetNullableDecimal("NominalValue");
|
||||||
|
info.Premium = r.GetStringSafe("Premium");
|
||||||
|
info.Note = r.GetStringSafe("Note");
|
||||||
|
info.CapitalValue = r.GetStringSafe("CapitalValue");
|
||||||
|
info.AutocallValue = r.GetStringSafe("AutocallValue");
|
||||||
|
info.Memory = r.GetStringSafe("Memory");
|
||||||
|
info.RTS = r.GetStringSafe("RTS");
|
||||||
|
info.Var95 = r.GetStringSafe("Var95");
|
||||||
|
info.BufferKProt = r.GetStringSafe("BufferKProt");
|
||||||
|
info.BufferCPNProt = r.GetStringSafe("BufferCPNProt");
|
||||||
|
info.IRR = r.GetStringSafe("IRR");
|
||||||
|
info.Airbag = r.GetStringSafe("Airbag");
|
||||||
|
info.FrequenzaCedole = r.GetStringSafe("FrequenzaCedole");
|
||||||
|
info.LivelloBarriera = r.GetStringSafe("LivelloBarriera");
|
||||||
|
info.DataEmissione = r.GetStringSafe("DataEmissione");
|
||||||
|
info.RendimentoTotale = r.GetStringSafe("RendimentoTotale");
|
||||||
|
info.ValoreRimborso = r.GetStringSafe("ValoreRimborso");
|
||||||
|
info.DataRimborso = r.GetStringSafe("DataRimborso");
|
||||||
|
info.Leva = r.GetStringSafe("Leva");
|
||||||
|
info.FattoreAirbag = r.GetStringSafe("FattoreAirbag");
|
||||||
|
info.TriggerOneStar = r.GetStringSafe("TriggerOneStar");
|
||||||
|
info.PrezzoEmissione = r.GetNullableDecimal("PrezzoEmissione");
|
||||||
|
info.CpnInMemoria = r.GetNullableDecimal("CpnInMemoria");
|
||||||
|
info.CpnDaPagare = r.GetNullableDecimal("CpnDaPagare");
|
||||||
|
info.CpnPagati = r.GetNullableDecimal("CpnPagati");
|
||||||
|
info.RendimentoAttuale = r.GetStringSafe("RendimentoAttuale");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Sottostanti (rpt_Details_UL_ISIN) ──────────────────────
|
||||||
|
await using (var cmd = new SqlCommand("rpt_Details_UL_ISIN", conn) { CommandType = CommandType.StoredProcedure })
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@ISIN", isin);
|
||||||
|
|
||||||
|
await using var r = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
info.Sottostanti.Add(new Sottostante
|
||||||
|
{
|
||||||
|
Nome = r.GetStringSafe("Nome"),
|
||||||
|
Strike = r.GetStringSafe("Strike"),
|
||||||
|
LastPrice = r.GetStringSafe("LastPrice"),
|
||||||
|
Performance = r.GetStringSafe("Performance"),
|
||||||
|
CapitalBarrier = r.GetStringSafe("CapitalBarrier"),
|
||||||
|
CouponBarrier = r.GetStringSafe("CouponBarrier"),
|
||||||
|
TriggerAutocall = r.GetStringSafe("TriggerAutocall"),
|
||||||
|
ULCapitalBarrierBuffer = r.GetStringSafe("ULCapitalBarrierBuffer"),
|
||||||
|
ULCouponBarrierBuffer = r.GetStringSafe("ULCouponBarrierBuffer"),
|
||||||
|
ULTriggerAutocallDistance = r.GetStringSafe("ULTriggerAutocallDistance"),
|
||||||
|
DividendExDate = r.GetStringSafe("DividendExDate"),
|
||||||
|
DividendPayDate = r.GetStringSafe("DividendPayDate"),
|
||||||
|
DividendAmount = r.GetStringSafe("DividendAmount"),
|
||||||
|
DividendYield = r.GetStringSafe("DividendYield"),
|
||||||
|
DividendFutAmount = r.GetStringSafe("DividendFutAmount"),
|
||||||
|
DividendFutYield = r.GetStringSafe("DividendFutYield"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Anagrafica caricata per {Isin}: {Emittente}, {Count} sottostanti",
|
||||||
|
isin, info.Emittente, info.Sottostanti.Count);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Eventi ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public async Task<List<CertificateEvent>> GetCertificateEventsAsync(string isin)
|
||||||
|
{
|
||||||
|
var events = new List<CertificateEvent>();
|
||||||
|
|
||||||
|
await using var conn = new SqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
await using var cmd = new SqlCommand("rpt_Events_CFT_ISIN", conn) { CommandType = CommandType.StoredProcedure };
|
||||||
|
cmd.Parameters.AddWithValue("@ISIN", isin);
|
||||||
|
|
||||||
|
await using var r = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
events.Add(new CertificateEvent
|
||||||
|
{
|
||||||
|
ObservationDate = r.GetStringSafe("ObservationDate"),
|
||||||
|
ExDate = r.GetStringSafe("ExDate"),
|
||||||
|
RecordDate = r.GetStringSafe("RecordDate"),
|
||||||
|
PaymentDate = r.GetStringSafe("PaymentDate"),
|
||||||
|
CouponTrigger = r.GetStringSafe("CouponTrigger"),
|
||||||
|
CouponValue = r.GetStringSafe("CouponValue"),
|
||||||
|
Paid = r.GetStringSafe("Paid"),
|
||||||
|
Memory = r.GetStringSafe("Memory"),
|
||||||
|
AmountPaid = r.GetStringSafe("AmountPaid"),
|
||||||
|
CapitalTrigger = r.GetStringSafe("CapitalTrigger"),
|
||||||
|
CapitalValue = r.GetStringSafe("CapitalValue"),
|
||||||
|
AutocallTrigger = r.GetStringSafe("AutocallTrigger"),
|
||||||
|
AutocallValue = r.GetStringSafe("AutocallValue"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Eventi caricati per {Isin}: {Count} eventi", isin, events.Count);
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Analisi Rischio / Scenario ────────────────────────────────────
|
||||||
|
|
||||||
|
public async Task<ScenarioAnalysis> GetScenarioAnalysisAsync(string isin)
|
||||||
|
{
|
||||||
|
var scenario = new ScenarioAnalysis();
|
||||||
|
|
||||||
|
await using var conn = new SqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
await using var cmd = new SqlCommand("rpt_AnalisiScenario_ISIN", conn) { CommandType = CommandType.StoredProcedure };
|
||||||
|
cmd.Parameters.AddWithValue("@ISIN", isin);
|
||||||
|
|
||||||
|
await using var r = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
var row = new ScenarioRow
|
||||||
|
{
|
||||||
|
Label = r.GetStringSafe("Descrizione"),
|
||||||
|
Values = new List<string>
|
||||||
|
{
|
||||||
|
r.GetStringSafe("col1"),
|
||||||
|
r.GetStringSafe("col2"),
|
||||||
|
r.GetStringSafe("col3"),
|
||||||
|
r.GetStringSafe("col4"),
|
||||||
|
r.GetStringSafe("col5"),
|
||||||
|
r.GetStringSafe("col6"),
|
||||||
|
r.GetStringSafe("col7"),
|
||||||
|
r.GetStringSafe("col8"),
|
||||||
|
r.GetStringSafe("col9"),
|
||||||
|
r.GetStringSafe("col10"),
|
||||||
|
r.GetStringSafe("col11"),
|
||||||
|
r.GetStringSafe("col12"),
|
||||||
|
r.GetStringSafe("col13"),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
scenario.Rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Scenario caricato per {Isin}: {Count} righe", isin, scenario.Rows.Count);
|
||||||
|
return scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Utility ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public async Task<bool> IsScenarioAnalysisAllowedAsync(string isin)
|
||||||
|
{
|
||||||
|
// Se la SP rpt_AnalisiRischio_ISIN restituisce righe vuote,
|
||||||
|
// il certificato è di tipo Protection e non mostra l'analisi scenario.
|
||||||
|
// In alternativa, puoi usare una SP dedicata o controllare la Categoria.
|
||||||
|
var scenario = await GetScenarioAnalysisAsync(isin);
|
||||||
|
return scenario.Rows.Any(row => row.Values.Any(v => !string.IsNullOrWhiteSpace(v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> FindIsinByAliasIdAsync(string aliasId)
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
await using var cmd = new SqlCommand("rpt_FindIsinbyAliasID", conn) { CommandType = CommandType.StoredProcedure };
|
||||||
|
cmd.Parameters.AddWithValue("@AliasID", aliasId);
|
||||||
|
|
||||||
|
var result = await cmd.ExecuteScalarAsync();
|
||||||
|
return result?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// Extension methods per SqlDataReader (null-safe)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
public static class SqlDataReaderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Legge un campo stringa, restituisce string.Empty se NULL o colonna inesistente.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetStringSafe(this SqlDataReader reader, string column)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ordinal = reader.GetOrdinal(column);
|
||||||
|
if (reader.IsDBNull(ordinal)) return string.Empty;
|
||||||
|
// Gestisce sia varchar che altri tipi convertendoli a stringa
|
||||||
|
var value = reader.GetValue(ordinal);
|
||||||
|
return value?.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
catch (IndexOutOfRangeException)
|
||||||
|
{
|
||||||
|
// Colonna non presente nel resultset
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legge un campo decimal nullable.
|
||||||
|
/// </summary>
|
||||||
|
public static decimal? GetNullableDecimal(this SqlDataReader reader, string column)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ordinal = reader.GetOrdinal(column);
|
||||||
|
if (reader.IsDBNull(ordinal)) return null;
|
||||||
|
return Convert.ToDecimal(reader.GetValue(ordinal));
|
||||||
|
}
|
||||||
|
catch (IndexOutOfRangeException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Legge un campo generico con tipo specifico.
|
||||||
|
/// </summary>
|
||||||
|
public static T GetSafe<T>(this SqlDataReader reader, string column) where T : struct
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ordinal = reader.GetOrdinal(column);
|
||||||
|
if (reader.IsDBNull(ordinal)) return default;
|
||||||
|
return (T)Convert.ChangeType(reader.GetValue(ordinal), typeof(T));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using Microsoft.Data.SqlClient;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recupera i dati per il grafico dal database.
|
||||||
|
///
|
||||||
|
/// SP utilizzate:
|
||||||
|
/// - FSWeb_Chart_UL: Info sottostanti (IDCertificates, IDUnderlyings, StartDate, Strike, Barriere, Nome)
|
||||||
|
/// - FSWeb_Chart_DailyCTF: Performance giornaliera del certificato (Px_date, Performance)
|
||||||
|
/// - FSWeb_Chart_DailyUL: Performance giornaliera di ogni sottostante (Px_date, Performance)
|
||||||
|
/// </summary>
|
||||||
|
public interface IChartDataService
|
||||||
|
{
|
||||||
|
Task<ChartRenderData?> GetChartDataAsync(string isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ChartDataService : IChartDataService
|
||||||
|
{
|
||||||
|
private readonly string _connectionString;
|
||||||
|
private readonly ILogger<ChartDataService> _logger;
|
||||||
|
|
||||||
|
public ChartDataService(IConfiguration config, ILogger<ChartDataService> logger)
|
||||||
|
{
|
||||||
|
_connectionString = config.GetConnectionString("CertDb")
|
||||||
|
?? throw new InvalidOperationException("ConnectionString 'CertDb' non configurata.");
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ChartRenderData?> GetChartDataAsync(string isin)
|
||||||
|
{
|
||||||
|
await using var conn = new SqlConnection(_connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
// ── 1. Carica info sottostanti (FSWeb_Chart_UL) ────────────────
|
||||||
|
var ulInfos = new List<ChartUnderlyingInfo>();
|
||||||
|
|
||||||
|
await using (var cmd = new SqlCommand("FSWeb_Chart_UL", conn) { CommandType = CommandType.StoredProcedure })
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@isin", isin);
|
||||||
|
await using var r = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
ulInfos.Add(new ChartUnderlyingInfo
|
||||||
|
{
|
||||||
|
IDCertificates = r.GetInt32(r.GetOrdinal("IDCertificates")),
|
||||||
|
IDUnderlyings = r.GetInt32(r.GetOrdinal("IDUnderlyings")),
|
||||||
|
StartDate = r.GetDateTime(r.GetOrdinal("StartDate")),
|
||||||
|
Strike = r.GetDecimal(r.GetOrdinal("Strike")),
|
||||||
|
BarrieraCoupon = r.GetDecimal(r.GetOrdinal("BarrieraCoupon")),
|
||||||
|
BarrieraCapitale = r.GetDecimal(r.GetOrdinal("BarrieraCapitale")),
|
||||||
|
Sottostante = r.GetString(r.GetOrdinal("Sottostante")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ulInfos.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Nessun sottostante trovato per il grafico di {Isin} (meno di 30 prezzi EOD?)", isin);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chartData = new ChartRenderData
|
||||||
|
{
|
||||||
|
Isin = isin,
|
||||||
|
BarrieraCapitale = ulInfos[0].BarrieraCapitale,
|
||||||
|
BarrieraCoupon = ulInfos[0].BarrieraCoupon,
|
||||||
|
Strike = 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 2. Carica performance certificato (FSWeb_Chart_DailyCTF) ───
|
||||||
|
var ctfSeries = new ChartSeries { Name = isin, IsCertificate = true };
|
||||||
|
|
||||||
|
await using (var cmd = new SqlCommand("FSWeb_Chart_DailyCTF", conn) { CommandType = CommandType.StoredProcedure })
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@ISIN", isin);
|
||||||
|
await using var r = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
ctfSeries.Points.Add(new ChartDataPoint
|
||||||
|
{
|
||||||
|
Date = r.GetDateTime(r.GetOrdinal("px_date")),
|
||||||
|
Performance = r.GetDecimal(r.GetOrdinal("Performance")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ordina per data ascendente (la SP restituisce DESC)
|
||||||
|
ctfSeries.Points.Sort((a, b) => a.Date.CompareTo(b.Date));
|
||||||
|
chartData.Series.Add(ctfSeries);
|
||||||
|
|
||||||
|
// ── 3. Carica performance per ogni sottostante (FSWeb_Chart_DailyUL) ──
|
||||||
|
foreach (var ul in ulInfos)
|
||||||
|
{
|
||||||
|
var ulSeries = new ChartSeries
|
||||||
|
{
|
||||||
|
Name = ul.Sottostante.Replace(" ", string.Empty),
|
||||||
|
IsCertificate = false
|
||||||
|
};
|
||||||
|
|
||||||
|
await using (var cmd = new SqlCommand("FSWeb_Chart_DailyUL", conn) { CommandType = CommandType.StoredProcedure })
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@IDCertificates", ul.IDCertificates);
|
||||||
|
cmd.Parameters.AddWithValue("@IDUnderlyings", ul.IDUnderlyings);
|
||||||
|
cmd.Parameters.Add("@StartDate", SqlDbType.Date).Value = ul.StartDate;
|
||||||
|
cmd.Parameters.AddWithValue("@Strike", (double)ul.Strike);
|
||||||
|
|
||||||
|
await using var r = await cmd.ExecuteReaderAsync();
|
||||||
|
while (await r.ReadAsync())
|
||||||
|
{
|
||||||
|
ulSeries.Points.Add(new ChartDataPoint
|
||||||
|
{
|
||||||
|
Date = r.GetDateTime(r.GetOrdinal("Px_date")),
|
||||||
|
Performance = r.GetDecimal(r.GetOrdinal("Performance")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ulSeries.Points.Sort((a, b) => a.Date.CompareTo(b.Date));
|
||||||
|
chartData.Series.Add(ulSeries);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation("Dati grafico caricati per {Isin}: {SeriesCount} serie, certificato {CftPoints} punti",
|
||||||
|
isin, chartData.Series.Count, ctfSeries.Points.Count);
|
||||||
|
|
||||||
|
return chartData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sezione 4: Grafico certificato/sottostanti/barriere.
|
||||||
|
///
|
||||||
|
/// Genera il grafico interamente in memoria con SkiaSharp,
|
||||||
|
/// lo inserisce come immagine in una pagina PDF landscape.
|
||||||
|
/// Elimina la dipendenza dall'endpoint esterno ChartFSWeb.aspx.
|
||||||
|
/// </summary>
|
||||||
|
public class ChartSectionRenderer : IChartSectionRenderer
|
||||||
|
{
|
||||||
|
private readonly IChartDataService _chartDataService;
|
||||||
|
private readonly ILogger<ChartSectionRenderer> _logger;
|
||||||
|
|
||||||
|
public ChartSectionRenderer(
|
||||||
|
IChartDataService chartDataService,
|
||||||
|
ILogger<ChartSectionRenderer> logger)
|
||||||
|
{
|
||||||
|
_chartDataService = chartDataService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<PdfDocument?> RenderAsync(string isin)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ── 1. Recupera dati dal DB ────────────────────────────────
|
||||||
|
var chartData = await _chartDataService.GetChartDataAsync(isin);
|
||||||
|
|
||||||
|
if (chartData == null || chartData.Series.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Nessun dato per il grafico di {Isin}. Sezione saltata.", isin);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Genera immagine PNG con SkiaSharp ───────────────────
|
||||||
|
byte[] pngBytes = SkiaChartRenderer.RenderToPng(chartData, 1100, 700);
|
||||||
|
|
||||||
|
_logger.LogInformation("Grafico generato per {Isin}: {Size} bytes PNG, {Series} serie",
|
||||||
|
isin, pngBytes.Length, chartData.Series.Count);
|
||||||
|
|
||||||
|
// ── 3. Inserisci nel PDF ───────────────────────────────────
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
doc.PageSettings.Size = PdfPageSize.A4;
|
||||||
|
doc.PageSettings.Orientation = PdfPageOrientation.Landscape;
|
||||||
|
doc.PageSettings.Margins.All = 30;
|
||||||
|
|
||||||
|
var page = doc.Pages.Add();
|
||||||
|
var g = page.Graphics;
|
||||||
|
float pageWidth = page.GetClientSize().Width;
|
||||||
|
float pageHeight = page.GetClientSize().Height;
|
||||||
|
|
||||||
|
// Carica immagine PNG
|
||||||
|
using var imgStream = new MemoryStream(pngBytes);
|
||||||
|
var pdfImage = new PdfBitmap(imgStream);
|
||||||
|
|
||||||
|
// Calcola dimensioni per fit in pagina mantenendo aspect ratio
|
||||||
|
float imgRatio = (float)pdfImage.Width / pdfImage.Height;
|
||||||
|
float drawWidth = pageWidth;
|
||||||
|
float drawHeight = drawWidth / imgRatio;
|
||||||
|
|
||||||
|
if (drawHeight > pageHeight)
|
||||||
|
{
|
||||||
|
drawHeight = pageHeight;
|
||||||
|
drawWidth = drawHeight * imgRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Centra verticalmente
|
||||||
|
float x = (pageWidth - drawWidth) / 2;
|
||||||
|
float y = (pageHeight - drawHeight) / 2;
|
||||||
|
|
||||||
|
g.DrawImage(pdfImage, x, y, drawWidth, drawHeight);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Errore nella generazione del grafico per ISIN {Isin}. Report senza grafico.", isin);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
using Syncfusion.Pdf.Grid;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sezione 2: Lista eventi del certificato (tabella multi-pagina con paginazione automatica).
|
||||||
|
/// Dati da: rpt_Events_CFT_ISIN
|
||||||
|
/// </summary>
|
||||||
|
public class EventiSectionRenderer : IPdfSectionRenderer
|
||||||
|
{
|
||||||
|
public string SectionName => "Eventi";
|
||||||
|
public int Order => 2;
|
||||||
|
|
||||||
|
public PdfDocument Render(CertificateReportData data)
|
||||||
|
{
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
doc.PageSettings.Size = PdfPageSize.A4;
|
||||||
|
doc.PageSettings.Orientation = PdfPageOrientation.Landscape;
|
||||||
|
doc.PageSettings.Margins.All = PdfTheme.PageMargin;
|
||||||
|
|
||||||
|
var page = doc.Pages.Add();
|
||||||
|
var g = page.Graphics;
|
||||||
|
float w = page.GetClientSize().Width;
|
||||||
|
float y = 0;
|
||||||
|
|
||||||
|
// ── Titolo ─────────────────────────────────────────────────────
|
||||||
|
g.DrawString("Lista Eventi", PdfTheme.SectionTitleFont,
|
||||||
|
new PdfSolidBrush(PdfTheme.SectionTitle), new RectangleF(0, y, w, 18));
|
||||||
|
y += 22;
|
||||||
|
|
||||||
|
// ── Griglia ────────────────────────────────────────────────────
|
||||||
|
var grid = new PdfGrid();
|
||||||
|
grid.Style.CellPadding = new PdfPaddings(3, 3, 2, 2);
|
||||||
|
|
||||||
|
// Colonne mappate sui campi della SP rpt_Events_CFT_ISIN
|
||||||
|
string[] headers =
|
||||||
|
{
|
||||||
|
"Osservazione", "Ex Date", "Record", "Pagamento",
|
||||||
|
"Trigger CPN", "Cedola %", "Pagato", "Memoria",
|
||||||
|
"Importo Pagato", "Trigger Autocall", "Valore Autocall"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var _ in headers) grid.Columns.Add();
|
||||||
|
|
||||||
|
// Header
|
||||||
|
var hr = grid.Headers.Add(1)[0];
|
||||||
|
for (int i = 0; i < headers.Length; i++)
|
||||||
|
{
|
||||||
|
hr.Cells[i].Value = headers[i];
|
||||||
|
hr.Cells[i].Style.Font = PdfTheme.Header;
|
||||||
|
hr.Cells[i].Style.BackgroundBrush = PdfTheme.HeaderBrush;
|
||||||
|
hr.Cells[i].Style.TextBrush = PdfTheme.HeaderTextBrush as PdfBrush;
|
||||||
|
hr.Cells[i].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Righe dati
|
||||||
|
for (int i = 0; i < data.Eventi.Count; i++)
|
||||||
|
{
|
||||||
|
var evt = data.Eventi[i];
|
||||||
|
var row = grid.Rows.Add();
|
||||||
|
|
||||||
|
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.CapitalTrigger;
|
||||||
|
//row.Cells[10].Value = evt.CapitalValue;
|
||||||
|
row.Cells[9].Value = evt.AutocallTrigger;
|
||||||
|
row.Cells[10].Value = evt.AutocallValue;
|
||||||
|
|
||||||
|
foreach (var cell in row.Cells.OfType<PdfGridCell>())
|
||||||
|
{
|
||||||
|
cell.Style.Font = PdfTheme.Small;
|
||||||
|
cell.StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Righe alternate
|
||||||
|
if (i % 2 == 1)
|
||||||
|
foreach (var cell in row.Cells.OfType<PdfGridCell>())
|
||||||
|
cell.Style.BackgroundBrush = PdfTheme.AlternateRowBrush;
|
||||||
|
|
||||||
|
// Evidenzia "SI" nella colonna Pagato
|
||||||
|
if (evt.Paid == "SI")
|
||||||
|
row.Cells[6].Style.TextBrush = PdfTheme.PositiveBrush as PdfBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 scale = w / total;
|
||||||
|
for (int i = 0; i < cw.Length; i++)
|
||||||
|
grid.Columns[i].Width = cw[i] * scale;
|
||||||
|
|
||||||
|
// Paginazione automatica con header ripetuto
|
||||||
|
grid.RepeatHeader = true;
|
||||||
|
var layout = new PdfGridLayoutFormat
|
||||||
|
{
|
||||||
|
Layout = PdfLayoutType.Paginate,
|
||||||
|
Break = PdfLayoutBreakType.FitPage
|
||||||
|
};
|
||||||
|
|
||||||
|
PdfTheme.ApplyThinBorders(grid);
|
||||||
|
|
||||||
|
grid.Draw(page, new PointF(0, y), layout);
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache in memoria per i PDF generati.
|
||||||
|
/// Evita di rigenerare lo stesso report se richiesto più volte
|
||||||
|
/// in un breve intervallo (es. utente che ricarica la pagina).
|
||||||
|
/// </summary>
|
||||||
|
public interface IPdfCacheService
|
||||||
|
{
|
||||||
|
byte[]? Get(string isin);
|
||||||
|
void Set(string isin, byte[] pdfBytes);
|
||||||
|
void Invalidate(string isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PdfCacheService : IPdfCacheService
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly TimeSpan _expiration;
|
||||||
|
private readonly ILogger<PdfCacheService> _logger;
|
||||||
|
|
||||||
|
public PdfCacheService(IMemoryCache cache, IConfiguration config, ILogger<PdfCacheService> logger)
|
||||||
|
{
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Default: 5 minuti, configurabile
|
||||||
|
var minutes = config.GetValue("ReportSettings:CacheMinutes", 5);
|
||||||
|
_expiration = TimeSpan.FromMinutes(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[]? Get(string isin)
|
||||||
|
{
|
||||||
|
var key = CacheKey(isin);
|
||||||
|
if (_cache.TryGetValue(key, out byte[]? cached) && cached != null)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Cache HIT per ISIN {Isin}", isin);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
_logger.LogDebug("Cache MISS per ISIN {Isin}", isin);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string isin, byte[] pdfBytes)
|
||||||
|
{
|
||||||
|
var options = new MemoryCacheEntryOptions()
|
||||||
|
.SetAbsoluteExpiration(_expiration)
|
||||||
|
.SetSize(pdfBytes.Length); // per limitare la memoria usata
|
||||||
|
|
||||||
|
_cache.Set(CacheKey(isin), pdfBytes, options);
|
||||||
|
_logger.LogDebug("Cache SET per ISIN {Isin} ({Size} bytes, TTL {Minutes}min)",
|
||||||
|
isin, pdfBytes.Length, _expiration.TotalMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate(string isin)
|
||||||
|
{
|
||||||
|
_cache.Remove(CacheKey(isin));
|
||||||
|
_logger.LogDebug("Cache INVALIDATE per ISIN {Isin}", isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CacheKey(string isin) => $"report_pdf_{isin}";
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unisce più PdfDocument in un singolo PDF usando Syncfusion.
|
||||||
|
/// Sostituisce PdfSharp CopyPages.
|
||||||
|
///
|
||||||
|
/// NOTA: gli stream temporanei devono restare aperti fino al Save() finale,
|
||||||
|
/// perché Syncfusion mantiene riferimenti lazy alle pagine importate.
|
||||||
|
/// </summary>
|
||||||
|
public class PdfMergerService : IPdfMergerService
|
||||||
|
{
|
||||||
|
public byte[] Merge(IEnumerable<PdfDocument> documents)
|
||||||
|
{
|
||||||
|
var finalDoc = new PdfDocument();
|
||||||
|
var tempStreams = new List<MemoryStream>();
|
||||||
|
var loadedDocs = new List<PdfLoadedDocument>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var doc in documents)
|
||||||
|
{
|
||||||
|
if (doc.Pages.Count == 0) continue;
|
||||||
|
|
||||||
|
// Salva il documento sorgente in uno stream temporaneo
|
||||||
|
var tempStream = new MemoryStream();
|
||||||
|
doc.Save(tempStream);
|
||||||
|
tempStream.Position = 0;
|
||||||
|
tempStreams.Add(tempStream);
|
||||||
|
|
||||||
|
// Carica come PdfLoadedDocument e importa le pagine
|
||||||
|
var loadedDoc = new PdfLoadedDocument(tempStream);
|
||||||
|
loadedDocs.Add(loadedDoc);
|
||||||
|
finalDoc.ImportPageRange(loadedDoc, 0, loadedDoc.Pages.Count - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salva il documento finale (tutti gli stream sono ancora aperti)
|
||||||
|
using var outputStream = new MemoryStream();
|
||||||
|
finalDoc.Save(outputStream);
|
||||||
|
return outputStream.ToArray();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Cleanup in ordine inverso: prima i loaded docs, poi gli stream
|
||||||
|
foreach (var loadedDoc in loadedDocs)
|
||||||
|
{
|
||||||
|
try { loadedDoc.Close(true); } catch { }
|
||||||
|
}
|
||||||
|
foreach (var stream in tempStreams)
|
||||||
|
{
|
||||||
|
try { stream.Dispose(); } catch { }
|
||||||
|
}
|
||||||
|
try { finalDoc.Close(true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orchestratore principale: coordina il flusso di generazione report.
|
||||||
|
///
|
||||||
|
/// Flusso:
|
||||||
|
/// 1. Recupera dati dal DB (stored procedures)
|
||||||
|
/// 2. Renderizza le sezioni PDF (anagrafica, eventi, scenario)
|
||||||
|
/// 3. Recupera/genera il grafico
|
||||||
|
/// 4. Unisce tutto in un unico PDF
|
||||||
|
/// </summary>
|
||||||
|
public class ReportOrchestrator : IReportOrchestrator
|
||||||
|
{
|
||||||
|
private readonly ICertificateDataService _dataService;
|
||||||
|
private readonly IEnumerable<IPdfSectionRenderer> _sectionRenderers;
|
||||||
|
private readonly IChartSectionRenderer _chartRenderer;
|
||||||
|
private readonly IPdfMergerService _merger;
|
||||||
|
private readonly IPdfCacheService _cache;
|
||||||
|
private readonly ILogger<ReportOrchestrator> _logger;
|
||||||
|
|
||||||
|
public ReportOrchestrator(
|
||||||
|
ICertificateDataService dataService,
|
||||||
|
IEnumerable<IPdfSectionRenderer> sectionRenderers,
|
||||||
|
IChartSectionRenderer chartRenderer,
|
||||||
|
IPdfMergerService merger,
|
||||||
|
IPdfCacheService cache,
|
||||||
|
ILogger<ReportOrchestrator> logger)
|
||||||
|
{
|
||||||
|
_dataService = dataService;
|
||||||
|
_sectionRenderers = sectionRenderers;
|
||||||
|
_chartRenderer = chartRenderer;
|
||||||
|
_merger = merger;
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<byte[]> GenerateReportAsync(string isin)
|
||||||
|
{
|
||||||
|
// ── Cache check ──────────────────────────────────────────────────
|
||||||
|
var cached = _cache.Get(isin);
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determina se lo scenario ha dati validi (evita doppia chiamata SP)
|
||||||
|
bool isScenarioAllowed = reportData.Scenario.Rows.Count > 0;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Dati recuperati per {Isin}: {SottostantiCount} sottostanti, {EventiCount} eventi, Scenario: {ScenarioAllowed}",
|
||||||
|
isin, reportData.Info.Sottostanti.Count, reportData.Eventi.Count, isScenarioAllowed);
|
||||||
|
|
||||||
|
// ── 2. Genera le sezioni PDF ───────────────────────────────────
|
||||||
|
var pdfSections = new List<PdfDocument>();
|
||||||
|
|
||||||
|
foreach (var renderer in _sectionRenderers.OrderBy(r => r.Order))
|
||||||
|
{
|
||||||
|
// Salta la sezione scenario se il certificato è Protection
|
||||||
|
if (renderer.SectionName == "Scenario" && !isScenarioAllowed)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Sezione Scenario saltata per {Isin} (certificato Protection)", isin);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sectionPdf = renderer.Render(reportData);
|
||||||
|
pdfSections.Add(sectionPdf);
|
||||||
|
_logger.LogInformation("Sezione '{Section}' generata per {Isin}", renderer.SectionName, isin);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Errore nella generazione della sezione '{Section}' per {Isin}",
|
||||||
|
renderer.SectionName, isin);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 3. Genera/recupera il grafico ──────────────────────────────
|
||||||
|
var chartPdf = await _chartRenderer.RenderAsync(isin);
|
||||||
|
if (chartPdf != null)
|
||||||
|
{
|
||||||
|
pdfSections.Add(chartPdf);
|
||||||
|
_logger.LogInformation("Sezione Grafico aggiunta per {Isin}", isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 4. Unisci tutto ────────────────────────────────────────────
|
||||||
|
var finalPdf = _merger.Merge(pdfSections);
|
||||||
|
|
||||||
|
_logger.LogInformation("Report generato per {Isin}: {Size} bytes, {Sections} sezioni",
|
||||||
|
isin, finalPdf.Length, pdfSections.Count);
|
||||||
|
|
||||||
|
// Salva in cache
|
||||||
|
_cache.Set(isin, finalPdf);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
foreach (var doc in pdfSections)
|
||||||
|
{
|
||||||
|
doc.Close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalPdf;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using CertReports.Syncfusion.Helpers;
|
||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
using Syncfusion.Drawing;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
using Syncfusion.Pdf.Graphics;
|
||||||
|
using Syncfusion.Pdf.Grid;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
public class ScenarioSectionRenderer : IPdfSectionRenderer
|
||||||
|
{
|
||||||
|
public string SectionName => "Scenario";
|
||||||
|
public int Order => 3;
|
||||||
|
|
||||||
|
// Gradiente header: da rosso scuro (-90%) a verde (+30%)
|
||||||
|
private static readonly Color[] HeaderGradient =
|
||||||
|
{
|
||||||
|
Color.FromArgb(255, 192, 0, 0), // -90% rosso scuro
|
||||||
|
Color.FromArgb(255, 220, 40, 20), // -80%
|
||||||
|
Color.FromArgb(255, 235, 80, 20), // -70%
|
||||||
|
Color.FromArgb(255, 245, 120, 15), // -60%
|
||||||
|
Color.FromArgb(255, 250, 160, 10), // -50%
|
||||||
|
Color.FromArgb(255, 255, 192, 0), // -40% giallo/arancio
|
||||||
|
Color.FromArgb(255, 255, 215, 0), // -30%
|
||||||
|
Color.FromArgb(255, 230, 220, 50), // -20%
|
||||||
|
Color.FromArgb(255, 200, 210, 60), // -10%
|
||||||
|
Color.FromArgb(255, 150, 195, 80), // 0%
|
||||||
|
Color.FromArgb(255, 100, 180, 80), // +10%
|
||||||
|
Color.FromArgb(255, 70, 165, 70), // +20%
|
||||||
|
Color.FromArgb(255, 50, 150, 50), // +30% verde
|
||||||
|
};
|
||||||
|
|
||||||
|
public PdfDocument Render(CertificateReportData data)
|
||||||
|
{
|
||||||
|
var doc = new PdfDocument();
|
||||||
|
doc.PageSettings.Size = PdfPageSize.A4;
|
||||||
|
doc.PageSettings.Orientation = PdfPageOrientation.Landscape;
|
||||||
|
doc.PageSettings.Margins.All = PdfTheme.PageMargin;
|
||||||
|
|
||||||
|
var page = doc.Pages.Add();
|
||||||
|
var g = page.Graphics;
|
||||||
|
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;
|
||||||
|
|
||||||
|
var scenario = data.Scenario;
|
||||||
|
if (scenario.Rows.Count == 0)
|
||||||
|
{
|
||||||
|
g.DrawString("Nessun dato scenario disponibile.", PdfTheme.Regular,
|
||||||
|
PdfTheme.TextBrush, new PointF(0, y));
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dataColCount = ScenarioAnalysis.VariationHeaders.Length;
|
||||||
|
int totalCols = dataColCount + 1;
|
||||||
|
|
||||||
|
var grid = new PdfGrid();
|
||||||
|
grid.Style.CellPadding = new PdfPaddings(4, 4, 3, 3);
|
||||||
|
|
||||||
|
for (int i = 0; i < totalCols; i++) grid.Columns.Add();
|
||||||
|
|
||||||
|
// ── Header: "Scenario da oggi:" + variazioni con gradiente ─────
|
||||||
|
var hr = grid.Headers.Add(1)[0];
|
||||||
|
hr.Cells[0].Value = "Scenario da oggi:";
|
||||||
|
hr.Cells[0].Style.Font = PdfTheme.SmallBold;
|
||||||
|
hr.Cells[0].Style.BackgroundBrush = new PdfSolidBrush(Color.FromArgb(255, 240, 240, 240));
|
||||||
|
|
||||||
|
for (int i = 0; i < dataColCount; i++)
|
||||||
|
{
|
||||||
|
hr.Cells[i + 1].Value = ScenarioAnalysis.VariationHeaders[i];
|
||||||
|
hr.Cells[i + 1].Style.Font = PdfTheme.SmallBold;
|
||||||
|
hr.Cells[i + 1].Style.BackgroundBrush = new PdfSolidBrush(HeaderGradient[i]);
|
||||||
|
hr.Cells[i + 1].Style.TextBrush = new PdfSolidBrush(Color.FromArgb(255, 255, 255, 255));
|
||||||
|
hr.Cells[i + 1].StringFormat = new PdfStringFormat(PdfTextAlignment.Center);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Righe dati ─────────────────────────────────────────────────
|
||||||
|
var altBg = new PdfSolidBrush(Color.FromArgb(255, 245, 245, 245));
|
||||||
|
var negativeBrush = new PdfSolidBrush(Color.FromArgb(255, 180, 30, 30));
|
||||||
|
var normalBrush = new PdfSolidBrush(Color.FromArgb(255, 60, 60, 60));
|
||||||
|
|
||||||
|
for (int r = 0; r < scenario.Rows.Count; r++)
|
||||||
|
{
|
||||||
|
var scenarioRow = scenario.Rows[r];
|
||||||
|
var row = grid.Rows.Add();
|
||||||
|
|
||||||
|
// Label
|
||||||
|
row.Cells[0].Value = scenarioRow.Label;
|
||||||
|
row.Cells[0].Style.Font = PdfTheme.SmallBold;
|
||||||
|
|
||||||
|
// Valori
|
||||||
|
for (int c = 0; c < Math.Min(scenarioRow.Values.Count, dataColCount); c++)
|
||||||
|
{
|
||||||
|
string val = scenarioRow.Values[c];
|
||||||
|
row.Cells[c + 1].Value = val;
|
||||||
|
row.Cells[c + 1].Style.Font = PdfTheme.Small;
|
||||||
|
row.Cells[c + 1].StringFormat = new PdfStringFormat(PdfTextAlignment.Right);
|
||||||
|
|
||||||
|
// Rosso per valori negativi
|
||||||
|
bool isNeg = !string.IsNullOrWhiteSpace(val) && val.Trim().StartsWith('-');
|
||||||
|
row.Cells[c + 1].Style.TextBrush = isNeg ? negativeBrush : normalBrush;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Righe alternate
|
||||||
|
if (r % 2 == 1)
|
||||||
|
{
|
||||||
|
for (int c = 0; c < totalCols; c++)
|
||||||
|
row.Cells[c].Style.BackgroundBrush = altBg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Larghezze colonne ──────────────────────────────────────────
|
||||||
|
float labelW = 130;
|
||||||
|
float dataColW = (w - labelW) / dataColCount;
|
||||||
|
grid.Columns[0].Width = labelW;
|
||||||
|
for (int i = 1; i < totalCols; i++)
|
||||||
|
grid.Columns[i].Width = dataColW;
|
||||||
|
|
||||||
|
PdfTheme.ApplyThinBorders(grid);
|
||||||
|
|
||||||
|
grid.Draw(page, new PointF(0, y));
|
||||||
|
return doc;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Implementations;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Genera il grafico certificato/sottostanti/barriere con SkiaSharp.
|
||||||
|
/// Produce un'immagine PNG in memoria, pronta per essere inserita nel PDF.
|
||||||
|
///
|
||||||
|
/// Replica il comportamento del vecchio ChartFSWeb.aspx:
|
||||||
|
/// - Serie spline per il certificato (linea nera, spessore 2)
|
||||||
|
/// - Serie spline per ogni sottostante (colori diversi, spessore 1)
|
||||||
|
/// - Linea costante rossa per Barriera Capitale
|
||||||
|
/// - Linea costante viola per Barriera Coupon (se diversa da Capitale)
|
||||||
|
/// - Linea costante verde per Strike (100%)
|
||||||
|
/// - Asse Y in percentuale, asse X con date
|
||||||
|
/// - Legenda in alto a destra
|
||||||
|
/// - Auto-scaling Y con margine 10%
|
||||||
|
/// </summary>
|
||||||
|
public static class SkiaChartRenderer
|
||||||
|
{
|
||||||
|
// Colori per le serie dei sottostanti (ciclici)
|
||||||
|
private static readonly SKColor[] SeriesColors =
|
||||||
|
{
|
||||||
|
new(183, 28, 28), // rosso scuro
|
||||||
|
new(139, 195, 74), // verde lime
|
||||||
|
new(103, 58, 183), // viola
|
||||||
|
new(0, 172, 193), // ciano
|
||||||
|
new(255, 152, 0), // arancione
|
||||||
|
new(121, 85, 72), // marrone
|
||||||
|
new(233, 30, 99), // rosa
|
||||||
|
new(0, 150, 136), // teal
|
||||||
|
};
|
||||||
|
|
||||||
|
// Font condivisi (creati una volta)
|
||||||
|
private static SKFont CreateFont(float size) =>
|
||||||
|
new(SKTypeface.FromFamilyName("Arial"), size);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Genera il grafico come immagine PNG.
|
||||||
|
/// </summary>
|
||||||
|
public static byte[] RenderToPng(ChartRenderData data, int width = 1100, int height = 700)
|
||||||
|
{
|
||||||
|
using var surface = SKSurface.Create(new SKImageInfo(width, height));
|
||||||
|
var canvas = surface.Canvas;
|
||||||
|
canvas.Clear(SKColors.White);
|
||||||
|
|
||||||
|
// ── Margini area di disegno ────────────────────────────────────
|
||||||
|
float marginLeft = 70;
|
||||||
|
float marginRight = 160; // spazio per legenda
|
||||||
|
float marginTop = 30;
|
||||||
|
float marginBottom = 50;
|
||||||
|
|
||||||
|
var plotArea = new SKRect(marginLeft, marginTop, width - marginRight, height - marginBottom);
|
||||||
|
|
||||||
|
// ── Calcola range assi ─────────────────────────────────────────
|
||||||
|
var (minDate, maxDate, minY, maxY) = CalculateRanges(data);
|
||||||
|
|
||||||
|
// ── Disegna griglia e assi ─────────────────────────────────────
|
||||||
|
DrawGrid(canvas, plotArea, minDate, maxDate, minY, maxY);
|
||||||
|
DrawAxisLabels(canvas, plotArea, minDate, maxDate, minY, maxY);
|
||||||
|
|
||||||
|
// ── Disegna linee costanti (barriere + strike) ─────────────────
|
||||||
|
var legendItems = new List<(string name, SKColor color, bool dashed, float thickness)>();
|
||||||
|
|
||||||
|
DrawHorizontalLine(canvas, plotArea, minY, maxY, (float)data.Strike, SKColors.Green, 1.5f, false);
|
||||||
|
legendItems.Add(("Strike", SKColors.Green, false, 1.5f));
|
||||||
|
|
||||||
|
DrawHorizontalLine(canvas, plotArea, minY, maxY, (float)data.BarrieraCapitale, SKColors.Red, 1.5f, false);
|
||||||
|
legendItems.Add(("Barriera Capitale", SKColors.Red, false, 1.5f));
|
||||||
|
|
||||||
|
if (data.BarrieraCoupon != data.BarrieraCapitale && data.BarrieraCoupon > 0)
|
||||||
|
{
|
||||||
|
DrawHorizontalLine(canvas, plotArea, minY, maxY, (float)data.BarrieraCoupon, new SKColor(128, 0, 128), 1.5f, false);
|
||||||
|
legendItems.Add(("Barriera Coupon", new SKColor(128, 0, 128), false, 1.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Disegna serie ──────────────────────────────────────────────
|
||||||
|
int colorIndex = 0;
|
||||||
|
foreach (var series in data.Series)
|
||||||
|
{
|
||||||
|
if (series.Points.Count < 2) continue;
|
||||||
|
|
||||||
|
SKColor color;
|
||||||
|
float thickness;
|
||||||
|
|
||||||
|
if (series.IsCertificate)
|
||||||
|
{
|
||||||
|
color = SKColors.Black;
|
||||||
|
thickness = 2.5f;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
color = SeriesColors[colorIndex % SeriesColors.Length];
|
||||||
|
thickness = 1.5f;
|
||||||
|
colorIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawSeries(canvas, plotArea, series, minDate, maxDate, minY, maxY, color, thickness);
|
||||||
|
legendItems.Insert(series.IsCertificate ? 0 : legendItems.Count - (data.BarrieraCoupon != data.BarrieraCapitale ? 3 : 2),
|
||||||
|
(series.Name, color, false, thickness));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Disegna bordo area plot ────────────────────────────────────
|
||||||
|
using var borderPaint = new SKPaint { Color = SKColors.Gray, StrokeWidth = 1, Style = SKPaintStyle.Stroke, IsAntialias = true };
|
||||||
|
canvas.DrawRect(plotArea, borderPaint);
|
||||||
|
|
||||||
|
// ── Disegna legenda ────────────────────────────────────────────
|
||||||
|
DrawLegend(canvas, plotArea, legendItems);
|
||||||
|
|
||||||
|
// ── Esporta PNG ────────────────────────────────────────────────
|
||||||
|
using var image = surface.Snapshot();
|
||||||
|
using var pngData = image.Encode(SKEncodedImageFormat.Png, 95);
|
||||||
|
return pngData.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Calcolo ranges
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static (DateTime minDate, DateTime maxDate, double minY, double maxY) CalculateRanges(ChartRenderData data)
|
||||||
|
{
|
||||||
|
DateTime minDate = DateTime.MaxValue, maxDate = DateTime.MinValue;
|
||||||
|
double minY = double.MaxValue, maxY = double.MinValue;
|
||||||
|
|
||||||
|
foreach (var series in data.Series)
|
||||||
|
{
|
||||||
|
foreach (var pt in series.Points)
|
||||||
|
{
|
||||||
|
if (pt.Date < minDate) minDate = pt.Date;
|
||||||
|
if (pt.Date > maxDate) maxDate = pt.Date;
|
||||||
|
double v = (double)pt.Performance;
|
||||||
|
if (v < minY) minY = v;
|
||||||
|
if (v > maxY) maxY = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Includi le linee costanti nel range
|
||||||
|
var constants = new List<double> { (double)data.Strike, (double)data.BarrieraCapitale };
|
||||||
|
if (data.BarrieraCoupon > 0) constants.Add((double)data.BarrieraCoupon);
|
||||||
|
|
||||||
|
foreach (var c in constants)
|
||||||
|
{
|
||||||
|
if (c < minY) minY = c;
|
||||||
|
if (c > maxY) maxY = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Margine 10%
|
||||||
|
double range = maxY - minY;
|
||||||
|
double margin = range * 0.1;
|
||||||
|
minY -= margin;
|
||||||
|
maxY += margin;
|
||||||
|
|
||||||
|
return (minDate, maxDate, minY, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Disegno griglia
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void DrawGrid(SKCanvas canvas, SKRect area, DateTime minDate, DateTime maxDate, double minY, double maxY)
|
||||||
|
{
|
||||||
|
using var gridPaint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = new SKColor(230, 230, 230),
|
||||||
|
StrokeWidth = 0.5f,
|
||||||
|
Style = SKPaintStyle.Stroke,
|
||||||
|
IsAntialias = true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Linee orizzontali
|
||||||
|
int ySteps = 8;
|
||||||
|
for (int i = 0; i <= ySteps; i++)
|
||||||
|
{
|
||||||
|
float y = area.Top + (area.Height / ySteps) * i;
|
||||||
|
canvas.DrawLine(area.Left, y, area.Right, y, gridPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linee verticali
|
||||||
|
var totalDays = (maxDate - minDate).TotalDays;
|
||||||
|
var step = totalDays > 1000 ? 365 : totalDays > 500 ? 180 : 90;
|
||||||
|
var d = new DateTime(minDate.Year, minDate.Month > 6 ? 7 : 1, 1);
|
||||||
|
while (d <= maxDate)
|
||||||
|
{
|
||||||
|
float x = DateToX(d, area, minDate, maxDate);
|
||||||
|
if (x >= area.Left && x <= area.Right)
|
||||||
|
canvas.DrawLine(x, area.Top, x, area.Bottom, gridPaint);
|
||||||
|
d = d.AddDays(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Labels assi (API moderna: SKFont + DrawText con textAlign)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void DrawAxisLabels(SKCanvas canvas, SKRect area, DateTime minDate, DateTime maxDate, double minY, double maxY)
|
||||||
|
{
|
||||||
|
using var font = CreateFont(11);
|
||||||
|
using var paint = new SKPaint { Color = SKColors.DimGray, IsAntialias = true };
|
||||||
|
|
||||||
|
// Asse Y
|
||||||
|
int ySteps = 8;
|
||||||
|
for (int i = 0; i <= ySteps; i++)
|
||||||
|
{
|
||||||
|
double val = maxY - ((maxY - minY) / ySteps) * i;
|
||||||
|
float y = area.Top + (area.Height / ySteps) * i;
|
||||||
|
string text = $"{val:F0} %";
|
||||||
|
canvas.DrawText(text, area.Left - 55, y + 4, SKTextAlign.Left, font, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asse X
|
||||||
|
var totalDays = (maxDate - minDate).TotalDays;
|
||||||
|
var step = totalDays > 1000 ? 365 : totalDays > 500 ? 180 : 90;
|
||||||
|
var d = new DateTime(minDate.Year, minDate.Month > 6 ? 7 : 1, 1);
|
||||||
|
while (d <= maxDate)
|
||||||
|
{
|
||||||
|
float x = DateToX(d, area, minDate, maxDate);
|
||||||
|
if (x >= area.Left && x <= area.Right)
|
||||||
|
{
|
||||||
|
string text = totalDays > 500 ? d.ToString("yyyy") : d.ToString("MMM yyyy");
|
||||||
|
canvas.DrawText(text, x, area.Bottom + 20, SKTextAlign.Center, font, paint);
|
||||||
|
}
|
||||||
|
d = d.AddDays(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Disegno serie
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void DrawSeries(SKCanvas canvas, SKRect area, ChartSeries series,
|
||||||
|
DateTime minDate, DateTime maxDate, double minY, double maxY, SKColor color, float thickness)
|
||||||
|
{
|
||||||
|
using var paint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = color,
|
||||||
|
StrokeWidth = thickness,
|
||||||
|
Style = SKPaintStyle.Stroke,
|
||||||
|
IsAntialias = true,
|
||||||
|
StrokeCap = SKStrokeCap.Round,
|
||||||
|
StrokeJoin = SKStrokeJoin.Round,
|
||||||
|
};
|
||||||
|
|
||||||
|
using var path = new SKPath();
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
foreach (var pt in series.Points)
|
||||||
|
{
|
||||||
|
float x = DateToX(pt.Date, area, minDate, maxDate);
|
||||||
|
float y = ValueToY((double)pt.Performance, area, minY, maxY);
|
||||||
|
y = Math.Max(area.Top, Math.Min(area.Bottom, y));
|
||||||
|
|
||||||
|
if (first)
|
||||||
|
{
|
||||||
|
path.MoveTo(x, y);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
path.LineTo(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.Save();
|
||||||
|
canvas.ClipRect(area);
|
||||||
|
canvas.DrawPath(path, paint);
|
||||||
|
canvas.Restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Linee orizzontali costanti
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void DrawHorizontalLine(SKCanvas canvas, SKRect area, double minY, double maxY,
|
||||||
|
float value, SKColor color, float thickness, bool dashed)
|
||||||
|
{
|
||||||
|
float y = ValueToY(value, area, minY, maxY);
|
||||||
|
if (y < area.Top || y > area.Bottom) return;
|
||||||
|
|
||||||
|
using var paint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = color,
|
||||||
|
StrokeWidth = thickness,
|
||||||
|
Style = SKPaintStyle.Stroke,
|
||||||
|
IsAntialias = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dashed)
|
||||||
|
paint.PathEffect = SKPathEffect.CreateDash(new[] { 8f, 4f }, 0);
|
||||||
|
|
||||||
|
canvas.DrawLine(area.Left, y, area.Right, y, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Legenda (API moderna: SKFont + DrawText con textAlign)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static void DrawLegend(SKCanvas canvas, SKRect plotArea, List<(string name, SKColor color, bool dashed, float thickness)> items)
|
||||||
|
{
|
||||||
|
float legendX = plotArea.Right + 15;
|
||||||
|
float legendY = plotArea.Top + 10;
|
||||||
|
float lineHeight = 20;
|
||||||
|
float lineWidth = 25;
|
||||||
|
|
||||||
|
using var font = CreateFont(10.5f);
|
||||||
|
using var textPaint = new SKPaint { Color = SKColors.DimGray, IsAntialias = true };
|
||||||
|
|
||||||
|
// Calcola larghezza massima testo
|
||||||
|
float maxTextWidth = items.Max(i => font.MeasureText(i.name));
|
||||||
|
float legendWidth = lineWidth + maxTextWidth + 20;
|
||||||
|
float legendHeight = items.Count * lineHeight + 10;
|
||||||
|
|
||||||
|
// Background legenda
|
||||||
|
using var bgPaint = new SKPaint { Color = new SKColor(255, 255, 255, 230), Style = SKPaintStyle.Fill };
|
||||||
|
using var borderPaint = new SKPaint { Color = new SKColor(200, 200, 200), Style = SKPaintStyle.Stroke, StrokeWidth = 0.5f };
|
||||||
|
|
||||||
|
var legendRect = new SKRect(legendX - 5, legendY - 5, legendX + legendWidth, legendY + legendHeight);
|
||||||
|
canvas.DrawRect(legendRect, bgPaint);
|
||||||
|
canvas.DrawRect(legendRect, borderPaint);
|
||||||
|
|
||||||
|
foreach (var (name, color, dashed, thickness) in items)
|
||||||
|
{
|
||||||
|
// Linea campione
|
||||||
|
using var linePaint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = color,
|
||||||
|
StrokeWidth = Math.Min(thickness, 2f),
|
||||||
|
Style = SKPaintStyle.Stroke,
|
||||||
|
IsAntialias = true,
|
||||||
|
};
|
||||||
|
if (dashed)
|
||||||
|
linePaint.PathEffect = SKPathEffect.CreateDash(new[] { 6f, 3f }, 0);
|
||||||
|
|
||||||
|
canvas.DrawLine(legendX, legendY + 8, legendX + lineWidth, legendY + 8, linePaint);
|
||||||
|
|
||||||
|
// Testo
|
||||||
|
canvas.DrawText(name, legendX + lineWidth + 6, legendY + 12, SKTextAlign.Left, font, textPaint);
|
||||||
|
legendY += lineHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
// Conversioni coordinate
|
||||||
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private static float DateToX(DateTime date, SKRect area, DateTime minDate, DateTime maxDate)
|
||||||
|
{
|
||||||
|
double totalDays = (maxDate - minDate).TotalDays;
|
||||||
|
if (totalDays == 0) return area.Left;
|
||||||
|
double ratio = (date - minDate).TotalDays / totalDays;
|
||||||
|
return area.Left + (float)(ratio * area.Width);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float ValueToY(double value, SKRect area, double minY, double maxY)
|
||||||
|
{
|
||||||
|
double range = maxY - minY;
|
||||||
|
if (range == 0) return area.MidY;
|
||||||
|
double ratio = (value - minY) / range;
|
||||||
|
return area.Bottom - (float)(ratio * area.Height);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
CertReports.Syncfusion/Services/Interfaces/IServices.cs
Normal file
50
CertReports.Syncfusion/Services/Interfaces/IServices.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using CertReports.Syncfusion.Models;
|
||||||
|
using Syncfusion.Pdf;
|
||||||
|
|
||||||
|
namespace CertReports.Syncfusion.Services.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recupera i dati del certificato dal database SQL Server (stored procedures)
|
||||||
|
/// </summary>
|
||||||
|
public interface ICertificateDataService
|
||||||
|
{
|
||||||
|
Task<CertificateInfo> GetCertificateInfoAsync(string isin);
|
||||||
|
Task<List<CertificateEvent>> GetCertificateEventsAsync(string isin);
|
||||||
|
Task<ScenarioAnalysis> GetScenarioAnalysisAsync(string isin);
|
||||||
|
Task<bool> IsScenarioAnalysisAllowedAsync(string isin);
|
||||||
|
Task<string?> FindIsinByAliasIdAsync(string aliasId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Genera una sezione PDF del report (anagrafica, eventi, scenario)
|
||||||
|
/// </summary>
|
||||||
|
public interface IPdfSectionRenderer
|
||||||
|
{
|
||||||
|
string SectionName { get; }
|
||||||
|
int Order { get; }
|
||||||
|
PdfDocument Render(CertificateReportData data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Genera la sezione grafico (può fallire, gestita separatamente)
|
||||||
|
/// </summary>
|
||||||
|
public interface IChartSectionRenderer
|
||||||
|
{
|
||||||
|
Task<PdfDocument?> RenderAsync(string isin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unisce più PdfDocument in un unico PDF
|
||||||
|
/// </summary>
|
||||||
|
public interface IPdfMergerService
|
||||||
|
{
|
||||||
|
byte[] Merge(IEnumerable<PdfDocument> documents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Orchestratore principale: coordina data retrieval, rendering e merge
|
||||||
|
/// </summary>
|
||||||
|
public interface IReportOrchestrator
|
||||||
|
{
|
||||||
|
Task<byte[]> GenerateReportAsync(string isin);
|
||||||
|
}
|
||||||
18
CertReports.Syncfusion/appsettings.Development.json
Normal file
18
CertReports.Syncfusion/appsettings.Development.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"CertDb": "Data Source=26.69.45.60;Database=FirstSolutionDB;User Id=sa;Password=Skyline72;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"MinimumLevel": {
|
||||||
|
"Default": "Debug",
|
||||||
|
"Override": {
|
||||||
|
"Microsoft": "Information",
|
||||||
|
"System": "Warning",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReportSettings": {
|
||||||
|
"CacheMinutes": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
33
CertReports.Syncfusion/appsettings.json
Normal file
33
CertReports.Syncfusion/appsettings.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"CertDb": "Data Source=26.69.45.60;Database=FirstSolutionDB;User Id=sa;Password=Skyline72;TrustServerCertificate=True;"
|
||||||
|
},
|
||||||
|
"Syncfusion": {
|
||||||
|
"LicenseKey": "Ngo9BigBOggjGyl/VkV+XU9AclRDX3xKf0x/TGpQb19xflBPallYVBYiSV9jS3hTdURrWH9ccXRUQmhUUE91XA=="
|
||||||
|
},
|
||||||
|
"ChartService": {
|
||||||
|
"BaseUrl": "https://reports.smart-roots.net:4004",
|
||||||
|
"ChartEndpoint": "/ChartFSWeb.aspx?width=800&height=600&isin={0}&tipo=pdf"
|
||||||
|
},
|
||||||
|
"CryptoSettings": {
|
||||||
|
"Passphrase": "ddCE3hM9BNJXgwtj"
|
||||||
|
},
|
||||||
|
"ReportSettings": {
|
||||||
|
"TempFolder": "wwwroot/temp",
|
||||||
|
"FontFamily": "Arial",
|
||||||
|
"PageMargin": 40,
|
||||||
|
"HeaderColor": "#2E5090",
|
||||||
|
"AlternateRowColor": "#F2F6FA",
|
||||||
|
"CacheMinutes": 5
|
||||||
|
},
|
||||||
|
"Serilog": {
|
||||||
|
"MinimumLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Override": {
|
||||||
|
"Microsoft": "Warning",
|
||||||
|
"System": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
30
CertReports.Syncfusion/docker-compose.yml
Normal file
30
CertReports.Syncfusion/docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
certreports:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5080:8080"
|
||||||
|
environment:
|
||||||
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
|
- ConnectionStrings__CertDb=Server=sqlserver;Database=CertDB;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;
|
||||||
|
- Syncfusion__LicenseKey=${SYNCFUSION_LICENSE_KEY}
|
||||||
|
- ChartService__BaseUrl=https://reports.smart-roots.net:4004
|
||||||
|
depends_on:
|
||||||
|
- sqlserver
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Opzionale: SQL Server locale per sviluppo
|
||||||
|
# Commentare se si usa un server SQL esterno
|
||||||
|
sqlserver:
|
||||||
|
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||||
|
environment:
|
||||||
|
- ACCEPT_EULA=Y
|
||||||
|
- MSSQL_SA_PASSWORD=YourStrong!Passw0rd
|
||||||
|
ports:
|
||||||
|
- "1433:1433"
|
||||||
|
volumes:
|
||||||
|
- sqldata:/var/opt/mssql
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
sqldata:
|
||||||
Reference in New Issue
Block a user