# Sizing & Capacity Planning Guide — NOVA Invoice Suite

> **🌐 Portale NOVA** — questo doc è anche disponibile in versione HTML premium nel [Documentation Portal](_portal/viewer.html?path=SIZING_GUIDE.md). Sizing rapido: [Premium Wizard](sizing-wizard/premium.html). Lifecycle interattivo: [Visualizer](lifecycle-visualizer/index.html).

> **Audience**: Architect, capacity planner, DevOps, customer engineer.
> **Versione**: 0.1 (2026-04-28)
> **Scope**: come dimensionare NOVA Invoice Suite per volumi enterprise (1-10M fatture/anno) — analisi colli di bottiglia, raccomandazioni resource, attivazione read replica analytics.
>
> 🧭 **Wizard interattivo**: per ottenere una raccomandazione di scenario + sizing + costi in 30 secondi, apri [`sizing-wizard/index.html`](sizing-wizard/index.html) in browser. Output basato sui calcoli di questa guida.

---

## Indice

1. [Volumetria di riferimento](#1-volumetria-di-riferimento)
2. [Bottleneck analysis per layer](#2-bottleneck-analysis-per-layer)
3. [Storage sizing (allegati + DB)](#3-storage-sizing-allegati--db)
4. [Read replica analytics](#4-read-replica-analytics)
5. [Tuning checklist 5M/anno](#5-tuning-checklist-5manno)
6. [Costi orientativi](#6-costi-orientativi)

---

## 1. Volumetria di riferimento

Esempio enterprise italiano con 5M fatture/anno e 5M allegati:

| Metrica | Valore steady-state | Picco fine-mese (3-5 giorni) |
|---|---|---|
| Throughput fatture | ~42/min (~0.7/s) | 75-200/min (1.3-3.3/s) |
| Throughput allegati | ~42/min | 75-200/min |
| Storage XML annuo | 250 GB | — |
| Storage allegati annuo | 4 TB | — |
| Storage DB structured annuo | 250 GB (~1 TB con HANA NSE) | — |
| Retention fiscale italiana | 10 anni → **40 TB allegati cumulati** | — |

**Distribuzione tipica FatturaPA italiana**:
- 30-40% del volume mensile concentrato negli ultimi 3-5 giorni del mese (scadenze fiscali, chiusura periodo).
- Ulteriore picco fine-trimestre (marzo/giugno/settembre/dicembre) ~20% sopra mese tipico.

**Mix formato allegati attesi**:
- ~30% XML FatturaPA puro (50 KB media) — niente OCR, parsing diretto
- ~60% PDF (500 KB - 2 MB media) — DOX OCR
- ~10% TIFF da scanner (1-5 MB) — DOX OCR

---

## 2. Bottleneck analysis per layer

Pipeline per fattura (end-to-end ~4-7 secondi):

```
Inbound → Parse XML → AI scoring → 3-way match → Workflow → Posting → Archiving → Audit
  ~50ms     ~200ms    2-5s         300-800ms     100-300ms  500-800ms  300ms       50ms
```

L'AI scoring è il single largest contributor (50-70% del wall-clock). Ottimizzazione → batch async (cron notturno) sposta il costo fuori dal critical path utente.

| Layer | Capacità nominale | Carico picco 5M/anno | Verdict |
|---|---|---|---|
| **CAP/Node pod** | ~1-2K req/s con DB I/O | 3.3 req/s | ✅ ampio margine, HPA 2→10 |
| **HANA Cloud HDI** (64 GB) | 100B+ righe, 50K qry/s | ~33 qry/s | ✅ trascurabile, serve solo partitioning |
| **PostgreSQL in-cluster** | 5-10K qry/s con tuning | 33 qry/s | ⚠️ partitioning per CC + anno obbligatorio |
| **AI Core (GenAI Hub GPT-4o)** | 10-60K TPM standard | ~6.75M TPM picco | ❌ tier enterprise OR async batch OR Ollama |
| **SAP DOX** | Tier standard 100K docs/mese | 416K/mese | ❌ tier enterprise OR pre-filtro XML |
| **S/4HANA OData (POST SupplierInvoice)** | 100-500 req/s rate limit | 3.3-10 req/s | ⚠️ batching + circuit breaker (già implementato) |
| **Event Mesh** | 5K msg/s standard | 3.3 msg/s | ✅ ampio margine |
| **DMS Storage** (BTP HTML5 Repo) | NON adatto a binary | 6.500 upload/giorno × 800KB | ❌ switch a S3/MinIO/BTP Object Store |

---

## 3. Storage sizing (allegati + DB)

### 3.1 Allegati binari

Default `DMS_ADAPTER=btp` (HTML5 Repo) **non scala** a 4 TB/anno: il repo HTML5 BTP è dimensionato per app static assets, non per documentary archive. Per volumi enterprise:

```yaml
# k8s/configmap.yaml — Kyma deploy
DMS_ADAPTER: s3                    # MinIO in-cluster OR BTP Object Store
S3_ENDPOINT: minio.nova.svc.cluster.local:9000
S3_BUCKET: nova-archive
S3_REGION: eu-west-1
```

| Backend | Costo orientativo 5 TB | Pro | Contro |
|---|---|---|---|
| **MinIO in-cluster** (Bitnami) | ~€100/mese | controllo totale, parte di k8s/overlays | gestione PVC + backup manuale |
| **BTP Object Store (managed)** | ~€125/mese (€0.025/GB) | managed, redundancy built-in | dipendenza BTP region |
| **AWS S3 (offering CSP)** | ~€115/mese (€0.023/GB) | ecosystem maturo | egress costs verso BTP |
| **Azure Blob (Hot)** | ~€100/mese | ecosystem maturo | egress costs |
| **HANA Cloud Lake** | ~€500/mese (€0.10/GB) | unified backup HANA | costo elevato per binary |

Tier-down strategy per retention 10y:
- Anno 1-2: **hot tier** (MinIO/S3 standard)
- Anno 3-7: **infrequent access** (S3 IA o Azure Cool, ~50% costo)
- Anno 8-10: **archive** (S3 Glacier o Azure Archive, ~10% costo, retrieval 1-12h)

### 3.2 DB structured (HANA / PostgreSQL)

Per HANA Cloud, partitioning obbligatorio sopra 1M righe:

```sql
-- db/migrations/hana/XXX_partition_invoices.sql
ALTER TABLE "sap.passive.invoice.GuidEdocInvoice" PARTITION BY
  HASH(CompanyCode) PARTITIONS 8,
  RANGE(YEAR(createdAt));

-- AuditLogEntry: solo RANGE per anno (append-only crescente)
ALTER TABLE "sap.passive.invoice.AuditLogEntry" PARTITION BY
  RANGE(YEAR(CreatedAt));

-- NSE (Native Storage Extension) per partition >2 anni — buffer pool offload
ALTER TABLE "sap.passive.invoice.GuidEdocInvoice" ALTER PARTITION (YEAR < 2024) PAGE LOADABLE;
```

Per PostgreSQL declarative partitioning:

```sql
-- db/migrations/pg/XXX_partition_invoices.sql
CREATE TABLE invoices_y2026 PARTITION OF "sap.passive.invoice.GuidEdocInvoice"
  FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
CREATE INDEX idx_invoices_y2026_cc ON invoices_y2026 (CompanyCode, ProcessingStatus);
```

Resource sizing:

| Volume | HANA Cloud | PostgreSQL in-cluster |
|---|---|---|
| <500K invoices/year | 32 GB instance | 4 GB RAM, 50 GB SSD |
| 500K-2M | 64 GB instance | 8 GB RAM, 100 GB SSD |
| 2-5M | **64-128 GB + NSE** | **16 GB RAM, 200 GB SSD + 1 read-replica** |
| 5-10M | 128-256 GB + NSE | 32 GB RAM, 500 GB SSD + 2 read-replica |

---

## 4. Read replica analytics

A 5M fatture/anno, le query aggregation (KPI dashboard, StrategicAnalytics multi-anno, BillingTotalsMonthly cross-CC) competono con l'OLTP write path per buffer pool e WAL. Sintomo tipico: dashboard refresh da 5 utenti in parallelo stalla le `confirmPhase` action, fino a hit dell'OData $batch timeout (30s default).

**Soluzione**: routing read-only di queste query a una **read replica** del DB.

### 4.1 Architettura

```
              ┌────────────────┐
              │ Action handler │  (analyzeRisk, postInvoice, …)
              │ read-modify-   │  → tx primary
              │ write          │
              └───────┬────────┘
                      │
                      ▼
              ┌────────────────┐    streaming  ┌──────────────────┐
              │ Primary DB     │──────────────►│ Replica DB       │
              │ (HANA / PG)    │  replication  │ (read-only)      │
              └────────────────┘  lag 1-30s    └──────────────────┘
                      ▲                                 ▲
                      │                                 │
              ┌───────┴────────┐                ┌───────┴────────┐
              │ Lifecycle path │                │ Analytics path │
              │ cds.tx(req)    │                │ getAnalyticsDb │
              │ via primary    │                │ via replica    │
              └────────────────┘                └────────────────┘
                                                analyticsActions.ts
                                                BillingTotalsMonthly
```

### 4.2 Helper di routing — `srv/integration/analyticsDb.ts`

Singleton helper con fallback graceful. Pseudocodice:

```typescript
async function getAnalyticsDb() {
  if (cds.requires['analytics-db']) {
    try { return await cds.connect.to('analytics-db'); }
    catch { return await cds.connect.to('db'); }   // graceful fallback
  }
  return await cds.connect.to('db');                // no replica configured
}
```

**Garanzie**:
- ✅ Se il binding `analytics-db` non esiste → uso trasparente del primary (dev locale, single-node deploy)
- ✅ Se il binding esiste ma il connect fallisce → log WARN + fallback al primary (replica unavailable degrade-to-primary, no 5xx)
- ✅ Memoization: connection cached al primo call, no overhead per request
- ✅ Routing per solo read-only: lifecycle action handler (write path) restano su primary

### 4.3 Entry point già instradati (2026-04-28)

| Handler / Entity | File | Tipo query |
|---|---|---|
| `getStatusDistribution` | `srv/handlers/analyticsActions.ts` | GROUP BY ProcessingStatus |
| `getChannelDistribution` | `srv/handlers/analyticsActions.ts` | GROUP BY AcquisitionChannel |
| `getOverviewKPIs` | `srv/handlers/analyticsActions.ts` | 8 COUNT(*) parallel |
| READ `ChannelKPIs` (view) | `srv/handlers/analyticsActions.ts` | aggregated view |
| READ `BillingTotalsMonthly` | `srv/handlers/adminAuditActions.ts` | aggregate over BillingSnapshot |

Espansione futura (NON ancora instradata, candidate):
- READ `StrategicAnalytics`, `SupplierPerformance`, `SupplierAnalytics` (views)
- READ `BillingCountersLive`, `ActiveCompanyCodesLive`
- Cron job `billingSnapshotJob` (può leggere via replica per generation)
- Forensic reads `AuditLogEntry` (escluso: serve consistenza al commit corrente, sta su primary)

### 4.4 Attivazione del binding

#### A. Cloud Foundry (`production` profile)

Aggiungi un service binding HANA Cloud read replica al manifest `mta-pg.yaml` o `mta.yaml`:

```yaml
# mta.yaml
modules:
  - name: nova-srv
    requires:
      - name: nova-hana
      - name: nova-hana-replica       # nuovo binding al replica HANA
        parameters:
          config:
            user-provided: true       # se replica è user-provided service

resources:
  - name: nova-hana-replica
    type: org.cloudfoundry.user-provided-service
    parameters:
      config:
        host: <replica-hana-host>.hana.ondemand.com
        port: 443
        user: NOVA_REPLICA_USER
        password: ${NOVA_REPLICA_PASSWORD}
        encrypt: true
```

In `package.json` `cds.requires`:

```json
{
  "cds": {
    "requires": {
      "analytics-db": {
        "kind": "hana",
        "[production]": {
          "credentials": {
            "host": "${HANA_REPLICA_HOST}",
            "port": "${HANA_REPLICA_PORT}",
            "user": "${HANA_REPLICA_USER}",
            "password": "${HANA_REPLICA_PASSWORD}",
            "schema": "${HANA_SCHEMA}",
            "encrypt": true,
            "validateCertificate": true
          }
        }
      }
    }
  }
}
```

oppure direttamente via CDS profile in `.cdsrc-private.json` (override locale):

```json
{
  "cds": {
    "requires": {
      "analytics-db": {
        "kind": "hana",
        "credentials": { "..." }
      }
    }
  }
}
```

#### B. Kyma (`k8s` profile)

Crea un `ServiceBinding` Kyma a una replica PG (Bitnami HA chart fornisce `<release>-postgresql-read` endpoint built-in):

```yaml
# k8s/servicebinding-analytics-db.yaml
apiVersion: services.cloud.sap.com/v1
kind: ServiceBinding
metadata:
  name: nova-analytics-db
  namespace: nova-invoice-suite
spec:
  serviceInstanceName: nova-postgresql-replica
  secretName: nova-analytics-db-binding
---
apiVersion: v1
kind: Secret
metadata:
  name: nova-analytics-db-binding
  namespace: nova-invoice-suite
type: Opaque
stringData:
  host: nova-postgresql-read.nova-invoice-suite.svc.cluster.local
  port: "5432"
  database: nova
  user: nova_readonly
  password: ${PG_REPLICA_PASSWORD}
```

E aggiungi il binding in `k8s/deployment.yaml`:

```yaml
spec:
  template:
    spec:
      containers:
        - name: nova-srv
          envFrom:
            - secretRef:
                name: nova-analytics-db-binding
                optional: true   # se replica non disponibile, container non fallisce
```

Nel `package.json`:

```json
{
  "cds": {
    "requires": {
      "analytics-db": {
        "[k8s]": {
          "kind": "postgres",
          "credentials": {
            "host": "${PG_REPLICA_HOST:nova-postgresql-read.nova-invoice-suite.svc.cluster.local}",
            "port": "5432",
            "database": "${PG_DB:nova}",
            "user": "${PG_REPLICA_USER}",
            "password": "${PG_REPLICA_PASSWORD}",
            "ssl": true
          }
        }
      }
    }
  }
}
```

#### C. Disattivazione (default)

**Niente da fare** — se nessun binding `analytics-db` è configurato, il helper resta in fallback al primary `db`. Tutto continua a funzionare in dev/test/single-node deploy.

### 4.5 Health check

L'helper espone `isReplicaBound()` per diagnostics. Aggiungi al `HealthService` se desideri monitoraggio:

```typescript
// srv/handlers/healthHandlers.ts
const { isReplicaBound } = require('../integration/analyticsDb');

healthSrv.on('READ', 'Status', async () => ({
  status: 'OK',
  analyticsReplica: isReplicaBound() ? 'enabled' : 'disabled (using primary)',
  // ... altri check ...
}));
```

### 4.6 Metriche raccomandate

In Kyma + Cloud Logging dashboard:

| Metrica | Soglia warning | Soglia critical |
|---|---|---|
| Replica lag (seconds) | > 30s | > 120s |
| Replica connect failures (5min window) | > 5 | > 20 |
| Analytics query p95 latency | > 2s | > 10s |
| Primary OLTP query p95 latency | > 500ms | > 2s |

Replica lag tipico:
- HANA Multi-AZ replica: <1s in steady state
- PostgreSQL streaming replication: 0.1-1s
- PostgreSQL logical replication: 1-30s

### 4.7 Trade-off & limitazioni

| Aspetto | Impatto |
|---|---|
| **Replica lag** (1-30s) | KPI dashboard può mostrare dati 5-30s vecchi. Già accettato dal cache TTL `KPI_TTL_SECS=300` (5 min). |
| **Network cost cross-region** | Se replica in altra region, latency dashboard +50-200ms. Posiziona replica in stessa region del primary. |
| **Schema sync** | Migration applicate sempre al primary; replica si aggiorna via streaming. NON applicare migration al replica direttamente. |
| **Failover scenario** | Se primary crasha e replica diventa primary, le scritture sul nuovo primary devono propagare. Drill quarterly in [DR_DRILL.md](runbooks/DR_DRILL.md). |
| **AuditLog forensic** | Per query forensic SOX, **non usare replica** — serve consistenza commit. Già escluso (auditImmutability.ts e MCP audit query usano primary). |

---

## 5. Tuning checklist 5M/anno

In ordine di priorità decrescente:

1. **DMS adapter = `s3` (MinIO o BTP Object Store)** — rimpiazza `btp` (HTML5 Repo) come backend allegati. **Required** sopra 1M/anno.
2. **DB partitioning** — HANA `HASH(CompanyCode) + RANGE(YEAR)` o PostgreSQL declarative `RANGE(date)`. **Required** sopra 2M/anno.
3. **Read replica analytics** — attiva binding `analytics-db` (vedi §4). **Strongly recommended** sopra 2M/anno.
4. **AI Core strategy** — async batch scoring (cron notturno) o tier enterprise dedicated capacity, o Ollama on-prem. **Required** sopra 1M/anno se vuoi mantenere costi <€10K/mese.
5. **DOX pre-filtering** — filtra fatture XML (parse diretto) → DOX solo per PDF/TIFF (~30% volume). Riduce tier DOX di ~70%.
6. **HPA tuning** — Kyma HPA `min: 2, max: 10, target: 70% CPU`. Già configurato.
7. **NSE per HANA** — partition >2 anni come `PAGE LOADABLE` per offload buffer pool.
8. **Tier-down storage** — S3 lifecycle policy: hot 2y → IA 6y → Archive 2y. Riduce costi 10y di ~70%.
9. **DR Tier 2/3** — warm-standby o hot-standby per RPO <30 min. **Required** per banche/PA italiane.
10. **Read replica analytics dedicata** per StrategicAnalytics multi-year scans (separata anche da KPI replica per workload isolation in deploy mega-scale 10M+/anno).

---

## 6. Costi orientativi annuali

Stima per **5M fatture/anno** su Kyma (production-ready):

| Componente | Tier consigliato | Costo annuo |
|---|---|---|
| Compute Kyma (4-8 pod, HPA) | t-shirt M | €8.000-12.000 |
| HANA Cloud 128 GB (+ NSE partition >2y) | enterprise | €25.000-30.000 |
| HANA replica (KPI/dashboard) | enterprise | €15.000-20.000 |
| PostgreSQL in-cluster (Bitnami HA) — alternativa HANA | self-hosted | €1.000-2.000 (storage) |
| Storage allegati (S3 hot 4 TB anno 1, IA tier 2-7) | AWS S3 | €4.000-8.000 |
| Storage allegati (cumulato 10y, tier-down) | run-rate dopo 10y | €15.000-25.000/anno |
| AI Core (async batch + Ollama on-prem) | mid | €24.000-40.000 |
| AI Core (real-time enterprise dedicated) | high | €60.000-100.000 |
| DOX (filtrato XML, ~125K docs/mese) | mid | €18.000-30.000 |
| Cloud Logging + ALM + ANS | standard | €4.000-8.000 |
| BTP services baseline (XSUAA, Destination, ecc.) | — | €6.000-10.000 |

**Range totale realistico**: **€80.000-200.000/anno** a seconda della strategia AI/DOX e tier replica.

Per **1M fatture/anno**: dividi per ~3-4 (no replica, AI batch, DOX standard).
Per **10M fatture/anno**: moltiplica per ~1.6-2 (replica dual, AI tier enterprise, DOX enterprise).

---

## Riferimenti

- [ARCHITECTURE.md](ARCHITECTURE.md) — architettura complessiva
- [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) — deploy step-by-step
- [adr/0002-archivelink-cs-storage-backing.md](adr/0002-archivelink-cs-storage-backing.md) — storage decision
- [runbooks/DR_DRILL.md](runbooks/DR_DRILL.md) — DR drill quarterly
- [`srv/integration/analyticsDb.ts`](../srv/integration/analyticsDb.ts) — read replica routing helper
