# Architecture — NOVA Invoice Suite

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

> Documentazione architetturale completa con diagrammi (Mermaid).
> Lettori: architetti / lead developer / DevOps / SAP integration team.
> Aggiornato 2026-04-28 (v4 — Wave 1+2 defense-in-depth, P1/P2/P3 audit fixes, governance v2, Repository Layer 42, Terraform IaC).
>
> Diagramma high-level: [`architecture/nova-invoice-suite-architecture-v4.svg`](architecture/nova-invoice-suite-architecture-v4.svg).

## Indice

1. [Vision e contesto](#1-vision-e-contesto)
2. [High-level architecture](#2-high-level-architecture)
3. [Stack tecnologico](#3-stack-tecnologico)
4. [Data model — entity-relationship](#4-data-model--entity-relationship)
5. [Lifecycle invoice — state machine](#5-lifecycle-invoice--state-machine)
6. [Service layer — Tier 1 / Tier 2](#6-service-layer--tier-1--tier-2)
7. [Pluggable adapter pattern](#7-pluggable-adapter-pattern)
8. [Sequence diagrams — flussi chiave](#8-sequence-diagrams--flussi-chiave)
9. [Authentication & authorization](#9-authentication--authorization)
9-bis. [Security cross-cutting](#9-bis-security-cross-cutting)
10. [Background jobs e event-driven](#10-background-jobs-e-event-driven)
11. [Multi-tenant strategy](#11-multi-tenant-strategy)
12. [Observability stack](#12-observability-stack)
13. [Deployment topology](#13-deployment-topology)

---

## 1. Vision e contesto

NOVA Invoice Suite è middleware **SAP CAP** (Node.js + TypeScript) tra:

- **10 Fiori Elements v4 app** (UI: 9 + platform SuperAdmin)
- **SAP S/4HANA Cloud / on-premise** (system of record)
- **Italian SDI** (Sistema di Interscambio — fatture elettroniche)
- **DOX, Conservatore, BPA, AI Core, Integration Suite** (servizi BTP)

Gestisce l'intero ciclo di vita di una fattura passiva (Vendor Invoice Management) — dalla ricezione SDI/DOX/EDI all'archiviazione sostitutiva (10 anni retention).

### Principi architetturali

1. **Standard-first**. Prima di custom, verificare se esiste API SAP rilasciata o servizio managed (BPA, DRC, MDG).
2. **Clean Core Level A**. Tutte le API S/4 consumate sono **C1-released**. Zero RFC/BAPI/user-exit.
3. **Pluggable adapters**. 12 famiglie di servizi (workflow, DMS, AI, ecc.) sono swappabili runtime.
4. **Multi-target deploy**. Stesso codebase boota su BTP CF / Kyma / on-prem via CDS profiles.
5. **Fail-closed defaults**. Security guards default ON, profile safety check a startup.

## 2. High-level architecture

```mermaid
flowchart TB
    subgraph Frontend["Fiori Elements v4 (10 apps)"]
        APP1[Manager UI<br/>main lifecycle]
        APP2[Approvals<br/>workflow inbox]
        APP3[Capture<br/>OCR/DOX]
        APP4[Governance<br/>rules engine]
        APP5[Setup<br/>parameters]
        APP6[Ops<br/>monitoring]
        APP7[Analytics<br/>BI dashboard]
        APP8[Overview<br/>landing KPI]
        APP9[Platform<br/>SuperAdmin]
    end

    subgraph Approuter["Approuter / Gateway"]
        AR[Approuter<br/>CSRF + session<br/>stickiness]
    end

    subgraph CAP["SAP CAP backend"]
        OS[OrchestratorService<br/>main Fiori service<br/>15 entità draft<br/>40+ readonly<br/>80 actions declared<br/>44 UI-exposed]
        WS[WorkflowService]
        IS[InboundService]
        HS[HealthService]
        PS[PlatformAdminService]
        T2[Tier-2 proxies<br/>BP/PO/SI/EDF/<br/>SES/AcctgDoc]
    end

    subgraph Adapters["Pluggable adapters"]
        AD1[Workflow]
        AD2[DMS]
        AD3[Notification]
        AD4[Messaging]
        AD5[AI]
        AD6[Cache]
        AD7[...]
    end

    subgraph DB["Database"]
        DBM[(HANA HDI / PostgreSQL / SQLite)]
    end

    subgraph External["External services"]
        S4[SAP S/4HANA Cloud<br/>14 OData endpoints]
        IAS[IAS / XSUAA / Keycloak<br/>OIDC + JWT]
        SDI[SDI Italia<br/>FatturaPA inbound]
        DMS[BTP DMS / S3 / Filesystem<br/>document storage]
        BPA[Build Process Automation<br/>workflow engine]
        DOX[Document Information<br/>Extraction]
        AI[AI Core<br/>GenAI]
    end

    subgraph Jobs["Background jobs"]
        J[12+ cron jobs<br/>autoAdvance, escalation,<br/>archive, billing, retention]
    end

    APP1 & APP2 & APP3 & APP4 & APP5 & APP6 & APP7 & APP8 & APP9 --> AR
    AR -->|XSUAA/OIDC JWT| OS
    AR --> WS & IS & HS & PS
    OS & WS & IS --> Adapters
    OS --> T2
    T2 -->|OData v4| S4
    Adapters --> DMS & BPA & DOX & AI
    Adapters & T2 --> DBM
    SDI -->|webhook| IS
    IAS -->|JWT validation| AR
    Jobs --> OS

    classDef sap fill:#0070f2,color:#fff
    classDef btp fill:#7e57c2,color:#fff
    classDef external fill:#ec7700,color:#fff
    class S4,SDI sap
    class IAS,DMS,BPA,DOX,AI btp
```

### Tier separation

- **Tier 1** — entità locali (DB), servono direttamente le Fiori app.
- **Tier 2** — proxy verso S/4HANA OData, estendono `BaseRemoteService` (circuit breaker + Integration Suite routing).

## 3. Stack tecnologico

| Layer | Tecnologia | Versione |
|---|---|---|
| Runtime | Node.js | 22 LTS (`.nvmrc`) |
| CAP | `@sap/cds` | ^9.8.5 |
| TypeScript | `typescript` | 6.x |
| DB drivers | `@cap-js/sqlite` 2.2 / `@cap-js/postgres` 2.2 / `@cap-js/hana` 2.7 | |
| Telemetry | `@cap-js/telemetry` | 1.6 (OpenTelemetry) |
| Frontend | SAPUI5 / Fiori Elements v4 | 1.136.16 |
| Auth | `@sap/xssec` | ^4 (XSUAA + OIDC) |
| Resilience | `@sap-cloud-sdk/resilience` | latest |
| AI | `@sap-ai-sdk/orchestration` | ^2.10 |
| Test | Jest | ^29 |
| Cache | ioredis | ^5.10 |
| Messaging | NATS | ^2.29 |

## 4. Data model — entity-relationship

```mermaid
erDiagram
    GuidEdocInvoice ||--o{ InvoiceLineItem : has
    GuidEdocInvoice ||--o{ InvoiceWorkflowItem : "approval workflow"
    GuidEdocInvoice ||--o{ AuditLogEntry : audited
    GuidEdocInvoice ||--o{ ProcessStepExecution : "step audit"
    GuidEdocInvoice ||--o{ InvoiceException : "open exceptions"
    GuidEdocInvoice ||--o{ ThreeWayMatchDetail : "PO match"
    GuidEdocInvoice ||--o{ BPResolutionCheck : "BP audit"
    GuidEdocInvoice ||--o{ FiscalValidationCheck : "fiscal audit"
    GuidEdocInvoice ||--o{ InvoiceCostAllocation : "NON_PO costing"
    GuidEdocInvoice ||--o{ CreditNoteMatch : "TD04 match"
    GuidEdocInvoice ||--o{ InvoiceAIInsight : "AI analysis"
    GuidEdocInvoice }o--|| ProcessingStatuses : status

    InvoiceWorkflowItem }o--|| ProcessRoles : "assigned role"
    InvoiceException }o--|| ProcessStepCheck : "raised by check"
    ProcessStepCheck }o--|| ProcessStep : "belongs to step"
    ProcessTemplate ||--o{ ProcessTemplateStep : composed
    ProcessTemplateStep }o--|| ProcessStep : "uses step"
    CompanyProcessConfig }o--|| ProcessTemplate : "active template"
    CompanyProcessConfig ||--o{ CompanyStepOverride : composed
    CompanyProcessConfig ||--o{ CompanyCheckOverride : composed
    ApprovalRule }o--|| ProcessRoles : "assigns approver"

    GuidEdocInvoice {
        uuid eDocumentGuid PK
        string CompanyCode "tenant key"
        string ProcessingStatus FK "STATUS_ORDER"
        string SDIDocumentType "TD01-TD28"
        string InvoiceCategory "PO|NON_PO|MIXED|CREDIT_PO"
        boolean IsCreditNote "TD04"
        decimal TotalAmount
        boolean IsActiveEntity "draft framework"
    }
    ProcessStepCheck {
        uuid ID PK
        string Code
        string DefaultSeverity "INFO|WARNING|ERROR"
        string OverridePolicy "AUTO_RESOLVE|SELF_SIGNED|REQUIRE_APPROVAL|NO_OVERRIDE"
        boolean IsPhaseGate "R3/D7"
    }
    ProcessStep {
        string ID PK
        string Name
        string BoundAction
        string ProducedStatus FK "STATUS_ORDER"
        string Category
        boolean IsDeviation "R6/D5"
    }
```

### Pattern di base

| Pattern | Esempi | Documentazione |
|---|---|---|
| `cuid` + `managed` | quasi tutte le entità transazionali | `@sap/cds/common` |
| Composition vs Association | `lineItems` Composition (cascade); `auditEntries` Association (no cascade) | [INVOICE_LIFECYCLE_DESIGN.md](INVOICE_LIFECYCLE_DESIGN.md) |
| Draft framework | `GuidEdocInvoice`, `ApprovalRule` | CAP `@odata.draft.enabled` |
| CodeList pattern | `ProcessingStatuses`, `RejectReasonCodes`, `ProcessRoles` | `sap.common.CodeList` + CSV seed |
| EAV (custom fields) | `CustomFieldDefinition` + `CustomFieldValue` | tenant-extensible |

> **Composition vs Association — regola d'oro**: per **audit/log/snapshot** (mai user-editable) usare `Association`. Per **child user-editable** (line items, allocations) usare `Composition`. Composition copia child rows in `_drafts` table → conflitti `duplicate key *_drafts_pkey` con audit concorrenti se misconfigurato.

## 5. Lifecycle invoice — state machine

### State diagram (canonical 6 phases + F3_BIS)

> **Architectural decisions 2026-04-28** — la state machine è stata semplificata:
> - **Single initial status `NEW`**: `RECEIVED`/`XML_PARSED` rimossi dal runtime (legacy enum, vedi migration 009).
> - **Terminali unificati**: `CANCELLED` → `REJECTED` con `RejectReasonCode`; `ERROR` → invoice resta in `POSTING_PENDING`/`PARKED` con `PostingError` field (migration 010).
> - **`REVERSED` solo da `POSTED`/`ARCHIVED`** via `reverseInvoice` (chiamata Cancel S/4).

```mermaid
stateDiagram-v2
    [*] --> NEW
    NEW --> BP_RESOLVED : confirmPhase (resolveBusinessPartner)
    BP_RESOLVED --> DUPLICATE_CHECK : confirmPhase
    DUPLICATE_CHECK --> VALIDATED : validateFiscal (PASSED|WARNING)
    VALIDATED --> RISK_ASSESSED : analyzeRisk
    RISK_ASSESSED --> MATCHING_PENDING : confirmPhase (PO)
    RISK_ASSESSED --> COST_ALLOCATION_PENDING : confirmPhase (NON_PO)
    RISK_ASSESSED --> CREDIT_MATCHING : confirmPhase (TD04 — F3_BIS)

    MATCHING_PENDING --> PO_MATCHED : matchPurchaseOrder
    PO_MATCHED --> THREE_WAY_OK : executeThreeWayMatch
    THREE_WAY_OK --> PENDING_APPROVAL : confirmPhase

    COST_ALLOCATION_PENDING --> COST_ALLOCATED : confirmCostAllocation (no validation)
    COST_ALLOCATION_PENDING --> COST_VALIDATED : confirmCostAllocation (validated)
    COST_ALLOCATED --> PENDING_APPROVAL : confirmPhase
    COST_VALIDATED --> PENDING_APPROVAL : confirmPhase

    CREDIT_MATCHING --> CREDIT_MATCHED : matchCreditNoteToOriginal (success)
    CREDIT_MATCHING --> CREDIT_MATCHING : matchCreditNoteToOriginal (no-match)
    CREDIT_MATCHED --> PENDING_APPROVAL : sendForApproval

    PENDING_APPROVAL --> APPROVED : approveInvoice
    PENDING_APPROVAL --> REWORK : rejectInvoice (allowRework)
    REWORK --> PENDING_APPROVAL : resolveBP/checkDup/...
    APPROVED --> POSTING_PENDING : confirmPhase
    APPROVED --> PARKED : parkInvoice
    POSTING_PENDING --> POSTED : postInvoice (S/4 INSERT)
    POSTING_PENDING --> POSTING_PENDING : posting failure (PostingError set)
    PARKED --> POSTED : postInvoice (S/4 Release)
    PARKED --> PARKED : release failure (PostingError set)
    POSTED --> ARCHIVED : archiveInvoice

    POSTED --> REVERSED : reverseInvoice (Cancel S/4)
    ARCHIVED --> REVERSED : reverseInvoice (Cancel S/4)

    NEW --> REJECTED : rejectAndCloseInvoice (reasonCode)
    BP_RESOLVED --> REJECTED : rejectAndCloseInvoice
    DUPLICATE_CHECK --> REJECTED : rejectAndCloseInvoice
    VALIDATED --> REJECTED : rejectAndCloseInvoice
    RISK_ASSESSED --> REJECTED : rejectAndCloseInvoice
    PENDING_APPROVAL --> REJECTED : rejectInvoice (allowRework=false)
    REJECTED --> NEW : reopenInvoice (default)
    REJECTED --> BP_RESOLVED : reopenInvoice (resumeStatus)

    ARCHIVED --> [*]
    REVERSED --> [*]
    REJECTED --> [*]
```

> **Posting failure handling**: il fallimento di `postInvoice`/Release **non** transita in uno stato dedicato.
> La fattura resta in `POSTING_PENDING` (o `PARKED`) con il campo `PostingError` valorizzato e
> un audit `POSTING_FAILED`. La UI mostra il messaggio inline; il retry è disponibile via la
> stessa action (`CanPostInvoice` esteso a `PARKED` per coprire il flusso Release).

### Phase gates (R3/D7 IsPhaseGate=true)

I phase gate sono check critici raised al boundary `confirmPhase`:

- **`COST_ALLOCATION_REQUIRED`** (NO_OVERRIDE) — fattura NON_PO senza `InvoiceCostAllocation` rows. Raised al boundary `→ PENDING_APPROVAL`.
  - **R8 fix**: il gate verifica esistenza righe allocazione PRIMA della rule lookup → se l'allocazione è già stata fatta, non scatta.
- **`CREDIT_NOTE_MATCH_MISSING`** (SELF_SIGNED) — TD04 senza match a fattura originale. Raised al boundary `→ CREDIT_MATCHING` (entry F3_BIS).
  - Override SELF_SIGNED disponibile (motivazione obbligatoria).

I phase gate **non sono disabilitabili** via `CompanyCheckOverride.IsDisabled` (R3/D17). Admin può solo downgradare `Severity` a WARNING per soft gating.

### Single vs Mass — parità (R9)

```mermaid
flowchart LR
    subgraph Single["Single — confirmPhase"]
        S1[user click]
        S2[buildPhaseBoundaries.next]
        S3[raisePhaseGates if PENDING_APPROVAL or CREDIT_MATCHING]
        S4[P2b ERROR check]
        S5[update status]
        S1 --> S2 --> S3 --> S4 --> S5
    end
    subgraph Mass["Mass — processNext / autoProcess"]
        M1[chain loop]
        M2[resolveNextAction status, invoice]
        M3[raisePhaseGatesIfNeeded]
        M4[STOP_ACTIONS check]
        M5[dispatch action]
        M1 --> M2 --> M3 --> M4 --> M5 --> M1
    end
    Single -. parità via SSOT .-> Mass

    style Single fill:#e3f2fd
    style Mass fill:#fff3e0
```

`STATUS_TO_NEXT_ACTION` (in `_adminShared.ts`) e `STOP_ACTIONS` sono single source of truth condivisi tra `processNext`, `autoProcess` e `autoAdvancementJob`.

## 6. Service layer — Tier 1 / Tier 2

### Tier 1 — Fiori-facing (entità locali)

| Servizio | Path | Scopo |
|---|---|---|
| `OrchestratorService` | `/odata/v4/orchestrator/` | Main Fiori service — 15 entità draft, 40+ read-only, 79 action, 30 CodeList |
| `WorkflowService` | `/odata/v4/workflow/` | Approval lifecycle (submit/approve/reject) + BPA callback |
| `InboundService` | `/odata/v4/inbound/` | REST endpoint multi-canale (SDI/DOX/EDI/Email/Webhook) |
| `HealthService` | `/health` | Liveness + readiness |
| `PlatformAdminService` | `/platform-admin/` | SuperAdmin-only (Certificate Manager, MinIO/S3 browser) |

### Tier 2 — Remote S/4HANA proxies

Tutti estendono `BaseRemoteService` (circuit breaker + Integration Suite routing).

| Servizio | API S/4 | Hostname tipo |
|---|---|---|
| `BusinessPartnerService` | `API_BUSINESS_PARTNER_0001` | `<tenant>.s4hana.cloud.sap` |
| `PurchaseOrderService` | `PURCHASEORDER_0001` (v4) | `<tenant>.s4hana.cloud.sap` |
| `ServiceEntrySheetService` | `API_SERVICE_ENTRY_SHEET_SRV` | `<tenant>.s4hana.cloud.sap` |
| `OperationalAcctgDocItemCubeService` | `API_OPLACCTGDOCITEMCUBE_SRV` | `<tenant>.s4hana.cloud.sap` |
| `SupplierInvoiceService` | `API_SUPPLIERINVOICE_PROCESS_SRV` (v4) | `<tenant>-api.s4hana.cloud.sap` |
| `ElectronicDocFileService` | `ELECTRONICDOCFILE_0001` (v4) | `<tenant>-api.s4hana.cloud.sap` |

### BaseRemoteService

```mermaid
classDiagram
    class BaseRemoteService {
        <<abstract>>
        +handleRead(req)
        +handleWrite(req)
        #_routedConnect(name) Service
        #_executeWithResilience(opts, fn)
        #_unwrapS4Error(err)
    }
    class SupplierInvoiceService {
        +init()
    }
    class PurchaseOrderService {
        +init()
    }
    class ElectronicDocFileService {
        +init()
        +injectFatturaPAFields(row)
    }
    BaseRemoteService <|-- SupplierInvoiceService
    BaseRemoteService <|-- PurchaseOrderService
    BaseRemoteService <|-- ElectronicDocFileService
```

`_executeWithResilience({id, timeout, circuitBreaker}, fn)` wraps every `remote.run(...)`. Senza, un endpoint S/4 stuck esaurirebbe il request loop in 60s.

## 7. Pluggable adapter pattern

12 famiglie di adapter — ogni famiglia ha factory + interface + N concrete classes.

```mermaid
classDiagram
    class WorkflowAdapter {
        <<interface>>
        +submit(payload)
        +approve(payload)
        +reject(payload)
        +requestInfo(payload)
    }
    class WorkflowAdapterFactory {
        +static create() WorkflowAdapter
    }
    class CAPWorkflowAdapter
    class BPAWorkflowAdapter
    class S4FlexWorkflowAdapter
    class NoOpWorkflowAdapter
    WorkflowAdapter <|.. CAPWorkflowAdapter
    WorkflowAdapter <|.. BPAWorkflowAdapter
    WorkflowAdapter <|.. S4FlexWorkflowAdapter
    WorkflowAdapter <|.. NoOpWorkflowAdapter
    WorkflowAdapterFactory ..> WorkflowAdapter : creates
```

### Selezione

```ts
// Factory legge cds.env.workflow.adapter o SystemParameters.WORKFLOW.WORKFLOW_ADAPTER
const wf = WorkflowAdapterFactory.create();
await wf.submit(payload);
```

CAP **boota sempre**, anche se binding mancante: factory cade su `NoOp` con LOG.warn. Mai `throw` a startup.

### 12 famiglie

| Famiglia | Adapters | Default per profile |
|---|---|---|
| Workflow | CAP, BPA, S4Flex, NoOp | `cap` |
| Inbound | DOX, Default, N8N, POPI, StructuredS4 | per channel |
| Extraction | DOX, CustomOCR, NoOp | BTP CF: `dox`; Kyma: `noop` |
| Documents (DMS) | BTP, CMIS, FileSystem, S3, ArchiveLink, NoOp | per profile |
| Notifications | ANS, SMTP, Alertmanager, NoOp | BTP: `ans`; Kyma: `alertmanager` |
| Messaging | EventMesh, NATS, S4Event, NoOp | BTP: `eventmesh`; Kyma: `nats` |
| Responsibility | CAP, RM | `cap` |
| AI | GenAIHub, Ollama, NoOp | `noop` |
| Cache | Redis, InMemory, NoOp | per profile |
| Audit | BTP, Stdout, NoOp | BTP: `btp`; Kyma: `stdout` |
| Monitoring | Prometheus, Stdout, NoOp | per profile |
| Transport | Manual, CTMS (custom + code) | `manual` |

> **Howto: aggiungere un adapter?** Vedi [DEVELOPER_GUIDE.md §Aggiungere un adapter pluggable](DEVELOPER_GUIDE.md#aggiungere-un-adapter-pluggable).

## 8. Sequence diagrams — flussi chiave

### 8.1 Single confirmPhase (user-driven)

```mermaid
sequenceDiagram
    actor U as User
    participant FE as Fiori v4
    participant AR as Approuter
    participant CAP as OrchestratorService
    participant H as confirmPhase handler
    participant PE as nonPoPhaseGate
    participant ER as exceptionRepo
    participant DB as Database

    U->>FE: click "Conferma Fase"
    FE->>AR: POST /odata/v4/orchestrator/Invoices(...)/confirmPhase
    AR->>CAP: dispatch (with JWT)
    CAP->>H: invoke
    H->>DB: SELECT FOR UPDATE
    DB-->>H: invoice row
    H->>H: buildPhaseBoundaries[status]
    H->>H: nextStatus = boundary.next(invoice)
    alt nextStatus is PENDING_APPROVAL or CREDIT_MATCHING
        H->>PE: raiseNonPoPhaseGateExceptions
        PE->>DB: check existing allocations (R8)
        PE->>ER: insert OPEN exception (if needed)
        PE-->>H: gates raised
    end
    H->>ER: findOpen severity=ERROR
    alt ERROR exceptions present
        H-->>FE: reject 409 (P2b block)
    else no blocking errors
        H->>DB: UPDATE ProcessingStatus
        H->>DB: INSERT AuditLogEntry post-commit
        H-->>FE: 200 + full invoice
    end
    FE-->>U: status updated
```

### 8.2 Mass processNext (chain auto-advance)

```mermaid
sequenceDiagram
    actor U as User
    participant FE as Fiori v4
    participant CAP as OrchestratorService
    participant PN as processNext
    participant SH as _adminShared
    participant H as action handlers
    participant PE as nonPoPhaseGate
    participant DB as Database

    U->>FE: select N rows + click "Process Next"
    loop for each row
        FE->>CAP: $batch POST processNext
        CAP->>PN: invoke
        PN->>DB: SELECT invoice FOR UPDATE
        PN->>SH: resolveNextAction(status, invoice)
        Note over SH: branch su SDIDocumentType<br/>e InvoiceCategory
        SH-->>PN: actionName
        loop until STOP or no next
            PN->>PE: raisePhaseGatesIfNeeded
            alt blocking ERROR
                PN-->>FE: stop early
            end
            PN->>SH: resolveNextAction
            alt STOP_ACTIONS
                PN->>PN: stop chain
            else
                PN->>H: srv.dispatch(action)
                H-->>PN: status updated
                PN->>DB: re-read invoice
            end
        end
        PN-->>FE: chain summary
    end
```

### 8.3 BP Resolution (Tier-2 chiamata)

```mermaid
sequenceDiagram
    participant H as resolveBP handler
    participant BR as BaseRemoteService
    participant CB as CircuitBreaker
    participant IS as Integration Suite
    participant S4 as S/4HANA BP

    H->>BR: srv.connect.to('BP_API')
    H->>BR: handleRead(query)
    BR->>CB: executeWithResilience
    CB->>CB: check state CLOSED/OPEN/HALF_OPEN
    alt CB OPEN
        CB-->>BR: fast-fail 503
        BR-->>H: throw with rejectSafe code
    else CB CLOSED or HALF_OPEN
        CB->>IS: route via IS proxy (if configured)
        IS->>S4: forward HTTP
        S4-->>IS: 200 OK + payload
        IS-->>CB: response
        CB-->>BR: result
        BR-->>H: parsed entities
    end
    H->>H: classifyConfidence(result)
    H->>DB: UPDATE invoice BP fields
```

### 8.4 Inbound SDI ricezione

```mermaid
sequenceDiagram
    participant SDI as Sistema di Interscambio
    participant FW as InboundService webhook
    participant IA as InboundAdapter
    participant XP as xmlParser FatturaPA
    participant J as autoAdvancementJob
    participant DB as Database

    SDI->>FW: POST /odata/v4/inbound/sdiReceive (FatturaPA XML)
    FW->>IA: parse(rawXml, headers)
    IA-->>FW: parsed DTO
    FW->>XP: extractInvoiceFields
    XP-->>FW: FatturaPA fields
    FW->>DB: INSERT GuidEdocInvoice status=NEW
    FW->>DB: INSERT InvoiceLineItem
    Note over J: cron 5min OR<br/>event-driven via NATS/EventMesh
    J->>DB: dispatch resolveBusinessPartner
    J->>DB: dispatch checkDuplicate
    J->>DB: ... auto-chain until stop point
```

### 8.5 Phase gate raise + auto-dispatch

```mermaid
sequenceDiagram
    participant H as confirmPhase
    participant PG as nonPoPhaseGate
    participant ER as exceptionRepo
    participant AD as autoDispatcher
    participant CAT as ProcessStepCheck
    participant CO as CompanyCheckOverride

    H->>PG: raiseNonPoPhaseGateExceptions
    PG->>PG: requiresCostAllocation
    PG->>DB: check InvoiceCostAllocation rows R8
    alt rows exist
        PG-->>H: gate inert
    else no rows
        PG->>DB: check AutoProcessingRule
        PG->>ER: dedupe
        PG->>ER: INSERT exception OPEN
        PG-->>H: raised gates
    end
    H->>AD: autoDispatch raisedGates
    AD->>CAT: lookup OverridePolicy + IsPhaseGate
    AD->>CO: lookup IsDisabled override
    alt IsPhaseGate (R3/D17)
        AD->>AD: ignore IsDisabled
    else AUTO_RESOLVE
        AD->>ER: bulkClose CLOSED_WITH_OVERRIDE
    else IsDisabled non-gate
        AD->>ER: bulkClose SYSTEM_DISABLED_CHECK
    end
```

## 9. Authentication & authorization

### Identity flow

```mermaid
flowchart LR
    U([User]) -->|login| IDP{Identity Provider}
    IDP -->|JWT| AR[Approuter]
    AR -->|JWT validation| CAP[CAP]
    CAP -->|"@restrict where CC = $user.CompanyCode"| DB[(DB)]

    subgraph Providers
        XSUAA[BTP CF — XSUAA]
        IAS[BTP Kyma — SAP IAS]
        KC[on-prem — Keycloak]
    end
    IDP -.- XSUAA
    IDP -.- IAS
    IDP -.- KC
```

### RBAC + SoD

15 ruoli canonici. Vedi [CONFIGURATION_GUIDE.md §4](CONFIGURATION_GUIDE.md#4-ruoli-canonici-e-rbac) per dettaglio + matrice.

```mermaid
flowchart TB
    subgraph Hierarchy
        SA[SuperAdmin cross-CC]
        AD[Admin CC scoped]
        AC[AdminConfig config + READ globale]
        OA[OperationalAdmin combo]
    end
    subgraph Approvers["Approver chain"]
        APP[Approver]
        APM[ApproverManager — override BP/PO]
        APT[ApproverTax — override fiscal]
        APS[ApproverSenior — high amount]
        APD[ApproverDirector]
        APC[ApproverCFO]
    end
    subgraph Operators
        V[Viewer]
        W[Worker]
        PO[PostingOfficer]
    end
    SoD["SoD mutex: PostingOfficer ⊥ Approver* ∪ Worker — OperationalAdmin EXEMPT"]
```

## 9-bis. Security cross-cutting

> **Posture corrente**: SOX/GDPR-COMPLIANT post Wave 1+2 hardening. Doc completa in [SECURITY.md](SECURITY.md). Pattern formalizzato in [ADR 0004](adr/0004-security-defense-in-depth.md).

### Defense-in-depth pattern (3-layer)

Ogni controllo critico (audit log integrity, file upload, secret export) è applicato in **3 layer indipendenti**:

```mermaid
flowchart LR
    Threat["Attacker / compromise"] -->|attempt| L1
    L1[CDS auth<br/>declarative @restrict] -->|bypass| L2
    L2[Application handler<br/>imperativo before/after] -->|bypass| L3
    L3[DB trigger / constraint<br/>storage-level] -->|✗ blocked| Reject([Reject + audit warn])

    classDef secure fill:#e3f2fd,stroke:#1565c0,color:#0d47a1;
    classDef threat fill:#ffebee,stroke:#c62828,color:#b71c1c;
    classDef result fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20;
    class L1,L2,L3 secure;
    class Threat threat;
    class Reject result;
```

| Asset critico | L1 (Gateway/CDS) | L2 (App handler) | L3 (Storage) |
|---|---|---|---|
| **AuditLog append-only** (A-1) | `services-auth.cds:298` SuperAdmin grant `READ` | `auditImmutability.ts` UPDATE/DELETE → 403 | DB trigger PG/HANA `nova_auditlog_block_dml` |
| **File upload validation** (M-1) | MIME whitelist | `_verifyMagicBytes` content check | DMS adapter `_safeResolveUnderBase` |
| **Secret export** (M-3) | CDS auth `exportConfiguration` | `IsSecret` flag mask | `_maskSecretFields` heuristic regex |
| **MCP query** (C-3) | APIRule + `mcpRouter` X-MCP-Token | `toolRegistry` CC whitelist | SQL filter `{ccField IN whitelist}` |

### Audit log dual-write (SOX)

```mermaid
flowchart LR
    Action[Lifecycle action] --> Logger[logAuditEntry]
    Logger --> Sanit[_sanitizePII<br/>IBAN/CF/PIVA/Email]
    Sanit -->|sync await| LocalDB[(Local DB)]
    Sanit -->|setImmediate| BTPAdapter[BTP Audit Adapter]
    BTPAdapter --> BTPLog[(BTP Audit Log<br/>tenant-wide)]

    LocalDB -.->|"3-layer<br/>append-only"| Forensic[Forensic query]
    BTPLog -.->|"tamper-evident"| Compliance[Compliance audit]

    classDef step fill:#e3f2fd,stroke:#1565c0,color:#0d47a1;
    classDef store fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c;
    classDef goal fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20;
    class Logger,Sanit,BTPAdapter step;
    class LocalDB,BTPLog store;
    class Forensic,Compliance goal;
```

PII sanitization (A-3) applicata pre-write con regex italiani FatturaPA: IBAN, Codice Fiscale, P.IVA, email → mascherati preservando ultimi 4 char per traceability supporto. Vedi [SECURITY.md §3.2](SECURITY.md#32-pii-sanitization-patterns-a-3).

### GDPR retention pipeline (A-4)

```mermaid
flowchart LR
    Cron[K8s CronJob<br/>monthly 03:17 UTC] -->|HTTP POST| Endpoint["/api/jobs/data-retention"]
    Endpoint -->|X-Job-Token| Job[processRetention]
    Job --> Cutoff[cutoff = now − 10y]
    Cutoff --> Inv[anonymize<br/>GuidEdocInvoice]
    Inv --> Children[anonymize children]
    Children --> Audit[AuditLogEntry<br/>OldValue/NewValue/Details]
    Children --> Comm[InvoiceComment<br/>CommentText/AuthorName]
    Children --> Exc[InvoiceException<br/>Description/Notes]

    classDef sched fill:#fff3e0,stroke:#ef6c00,color:#e65100;
    classDef proc fill:#e3f2fd,stroke:#1565c0,color:#0d47a1;
    classDef store fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c;
    class Cron,Endpoint sched;
    class Job,Cutoff,Inv,Children proc;
    class Audit,Comm,Exc store;
```

Italian fiscal law (D.lgs. 196/2003 + DPR 633/72) richiede 10y retention obbligatoria → anonymization sostituisce cancellazione GDPR Art. 17. Cronjob: [k8s/cronjob-data-retention.yaml](../k8s/cronjob-data-retention.yaml).

### Rate limit topology (M-6)

5 limiter dedicati su path distinti:

| Limiter | Path | Default | Key |
|---|---|---|---|
| Global | `*` | 200 req/min | user.id ÷ IP |
| Write | `/odata` POST/PATCH/DELETE | 60 req/min | user.id ÷ IP |
| Upload | `/inbound/receiveManualUpload` | 10 req/min | user.id ÷ IP |
| Jobs | `/api/jobs/*` | 30 req/min | IP only |
| MCP | `/mcp` | 60 req/min | mcpConsumer ÷ IP |

Redis store distributed across pod replicas, fallback in-memory in dev.

## 10. Background jobs e event-driven

```mermaid
flowchart TB
    subgraph Cron
        J1[delta-sync]
        J2[fi-match-enrichment]
        J3[escalation]
        J4[archive-outbox]
        J5[billing-snapshot]
        J6[data-retention]
        J7[invoice-purge]
        J8[ai-capture-purge]
        J9[dox-polling]
        J10[structured-s4-sync]
        J11[auto-advance]
        J12[emit-metrics]
    end
    subgraph EventDriven
        EM[Event Mesh / NATS]
        WH[Inbound webhooks]
    end
    Cron --> JR[(JobRunHistory)]
    EM --> OS[OrchestratorService]
    WH --> IS[InboundService]
    OS & IS -.-> EM
```

I cron sono triggerabili manualmente via `forceJobRun(jobName)` (Admin only) oppure via REST endpoint protetto da `X-Job-Token`.

## 11. Multi-tenant strategy

NOVA Invoice Suite è **single-tenant XSUAA** (`tenant-mode: dedicated`) ma **multi-CompanyCode**. Ogni utente ha 1+ CompanyCode in JWT claim `attr.CompanyCode`.

```mermaid
flowchart LR
    User -->|"JWT attr.CompanyCode"| Auth
    Auth -->|"@restrict where CC = $user.CompanyCode"| OS[OrchestratorService]
    OS -->|filter| DB[(GuidEdocInvoice)]
    SU[SuperAdmin] -->|"grant '*' no where"| OS_GLB[OrchestratorService global]
    OS_GLB -->|cross-CC| DB
    OS_GLB -.->|"emits SUPERADMIN_QUERY audit"| Audit[(AuditLogEntry)]
```

### Multi-region readiness

Hostname cluster Kyma parametrizzato via 2 secret GitHub (`KYMA_APP_URL` + `KYMA_KUBECONFIG`). Switch region = rotation secret + DNS repoint, zero code change.

## 12. Observability stack

```mermaid
flowchart LR
    CAP[CAP runtime] -->|cds.log| OTEL[OpenTelemetry SDK]
    CAP -->|metrics| MA[MonitoringAdapter]
    CAP -->|audit| AA[AuditAdapter]

    OTEL -->|OTLP HTTP| COL[OTel Collector]
    COL --> LOKI[(Loki logs)]
    COL --> TEMPO[(Tempo traces)]
    COL --> PROM[(Prometheus metrics)]

    MA -->|stdout JSON| LOKI
    AA -->|BTP managed / stdout| AUDIT_LOG[(BTP Audit Log)]

    LOKI & TEMPO & PROM --> GRAF[Grafana]
    GRAF -->|alerts| AM[Alertmanager / ANS]

    style OTEL fill:#7e57c2,color:#fff
    style GRAF fill:#fb8c00,color:#fff
```

| Profile | Telemetry (default) | Audit (default) | Metrics (default) | Cloud ALM compatibile? |
|---|---|---|---|---|
| `production` (BTP CF) | Cloud Logging managed | BTP Audit Log | Cloud ALM | ✅ nativo (entitlement BTP) |
| `k8s` (Kyma) | OTLP → Loki/Tempo/Prom | Stdout → Loki scrape | Prometheus + Grafana | ✅ **opt-in** (binding BTP service operator) |
| `onprem` | id. Kyma | id. | id. | ⚠️ richiede outbound connectivity → BTP (Cloud Connector o ingress pubblico) |
| `mocked`/`development` | NoOp | Stdout | NoOp | ❌ N/A |

**Nota su Cloud ALM**: la prima colonna mostra il *default* per profilo, non un vincolo tecnico. **Cloud ALM è additivo cross-profile** (qualsiasi runtime BTP-entitled può inviargli dati):

- **Health Monitoring** (heartbeat ping): API HTTP outbound, runtime-agnostic. Su Kyma → ServiceBinding `cloud-alm-svc` + secret env nel `nova-srv` deployment.
- **Real User Monitoring (RUM)**: snippet JavaScript injection nei `index.html` Fiori, completamente runtime-agnostic.
- **Integration & Exception Monitoring**: push API via `srv/monitoring/CloudALMAdapter.ts` (opt-in via `MONITORING_ADAPTER=cloud-alm` o composito `prometheus+cloud-alm`).
- **Job Automation Monitoring**: integrazione con `JobRunHistory` e push status agli endpoint Cloud ALM.

Quando ha senso attivare Cloud ALM su Kyma o on-prem:
- ✅ **Single pane of glass** cross-runtime: customer multi-deploy (es. dev su Kyma, prod su CF) vuole un'unica dashboard SAP-managed.
- ✅ **SAP support contract**: Cloud ALM è il canale ufficiale per ticket SAP support — dati operativi correlabili dall'engineer SAP.
- ✅ **Compliance regulated industry**: banking / PA che richiedono SAP-certified observability nei TIA (Transparency Information Acts).
- ⚠️ **Costo aggiuntivo**: ~€2-4K/anno entitlement, oltre allo stack Prometheus+Grafana già attivo.

Combinazioni tipiche (non esaustive):

| Scenario | Telemetry | Metrics | Cloud ALM | Razionale |
|---|---|---|---|---|
| Banking on Kyma | OTLP → Loki | Prometheus + Grafana **+ Cloud ALM** | ✅ | Audit Banca d'Italia + SAP support |
| PA on-prem | id. Kyma | Prometheus + Grafana | ❌ (air-gap) | Sovranità, no outbound |
| Privato cost-conscious Kyma | OTLP → Loki | Prometheus + Grafana | ❌ | Stack FOSS sufficiente |
| Multi-deploy enterprise | mix | mix | ✅ cross-cluster | Single pane of glass |

Sampling: dev `parent-based/always-on`; prod `traceidratio 0.1` (override `OTEL_TRACES_SAMPLER`).

## 13. Deployment topology

Vedi [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) per istruzioni step-by-step (BTP CF / Kyma / on-prem con dettagli IAS, Subaccount entitlements, role collections).

| Target | Profile | DB | Auth | CI coverage |
|---|---|---|---|---|
| BTP CF | `production` | HANA HDI | XSUAA | ⚠️ untested CI |
| BTP CF | `production-pg` | PostgreSQL | XSUAA | 🟡 nightly (deprecato 2026-Q4) |
| Kyma | `k8s` | PostgreSQL | OIDC (IAS) | ✅ primary CI |
| Kyma | `k8s-hana` | HANA on-prem | OIDC | ⚠️ manual |
| on-prem k3d | `k8s-onprem` | HANA / PG | OIDC (Keycloak) | ❌ manual |
| on-prem CF | `onprem` | HANA | XSUAA | ❌ manual |

---

**Documentazione correlata**:
- [CONFIGURATION_GUIDE.md](CONFIGURATION_GUIDE.md) — parametri/ruoli/template
- [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md) — istruzioni deploy step-by-step (subaccount + IAS + service plans)
- [DEVELOPER_GUIDE.md](DEVELOPER_GUIDE.md) — onboarding sviluppatore con howto pratici
- [API_REFERENCE.md](API_REFERENCE.md) — OData services + 80 actions (44 UI-exposed)
- [INVOICE_LIFECYCLE_DESIGN.md](INVOICE_LIFECYCLE_DESIGN.md) — design canonico processo
- [adr/](adr/) — Architectural Decision Records
- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) — common issues
