Search by

andreaballarin / acube-italy-receipts

andreaballarin

Acube Italy Receipts Library

Package info

github.com/andreaballarin/acube-italy-receipts

pkg:composer/andreaballarin/acube-italy-receipts

Statistics

Installs: 48

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.11 2026-07-16 12:51 UTC

This package is auto-updated.

Last update: 2026-09-16 13:05:38 UTC


README

SDK PHP 8.3+ per l'integrazione delle API Acube per gli Scontrini Elettronici (Documento Commerciale italiano).

Installazione

composer require andreaballarin/acube-italy-receipts

Requisiti

  • PHP: ^8.3
  • PSR-18 HTTP Client: Guzzle ^7.8 (incluso)
  • Certificati mTLS: Rilasciati da Acube per il tuo cash register

Quick Start

1. Login e ottieni il token JWT

use AndreaBallarin\ACubeItalyReceipts\{AuthenticationService, Environment};

$auth = new AuthenticationService(environment: Environment::Sandbox);
$token = $auth->login(
    email: 'your@email.com',
    password: 'your-password',
);

2. Setup del Client

use AndreaBallarin\ACubeItalyReceipts\Client;

$client = new Client(
    bearerToken: $token,
    environment: Environment::Sandbox,
    mtlsCertPath: '/path/to/cert.pem',
    mtlsKeyPath: '/path/to/key.pem',
    mtlsKeyPassphrase: 'passphrase-if-any', // opzionale
);

3. Registra il Client per l'utilizzo statico

use AndreaBallarin\ACubeItalyReceipts\Resources\Receipt;

Receipt::setClient($client);

4. Crea uno scontrino

use AndreaBallarin\ACubeItalyReceipts\Resources\{ReceiptDraft, ReceiptItem};

$item = new ReceiptItem(
    quantity: '1.00',
    description: 'Espresso',
    unitPrice: '1.20',
    vatRateCode: '10.00',
);

$draft = new ReceiptDraft(
    items: [$item],
    cashPaymentAmount: '1.20',
);

$receipt = Receipt::create($draft);
echo "UUID: " . $receipt->uuid;
echo "Documento: " . $receipt->documentNumber;

5. Recupera e annulla

$fetched = Receipt::get($receipt->uuid);
$voided = $fetched->void('Customer returned');

Onboarding: Merchant → PEM → Cash Register

Prima di poter emettere scontrini serve completare l'onboarding: creare un merchant, creare e attivare un PEM, poi creare il cash register che rilascia il certificato mTLS. Vedi examples/complete-supplier-onboarding.php per il flusso completo.

Importante: Merchant/Pem::create()/Pem::get()/Pem::activate()/CashRegister::create() usano la porta standard (nessun mTLS) — solo gli endpoint /mf1/receipts* (Receipt, ReceiptDetails) richiedono mTLS e vengono chiamati automaticamente sulla porta 444 quando il Client ha mtlsCertPath configurato. Usa quindi Client separati: uno senza mTLS per l'onboarding, uno con mTLS per gli scontrini. Poiché ApiResource::setClient() condivide un unico slot statico tra tutte le resource, richiama setClient() ogni volta che cambi Client.

1. Crea un merchant

use AndreaBallarin\ACubeItalyReceipts\Resources\{Merchant, MerchantDraft};
use AndreaBallarin\ACubeItalyReceipts\ValueObjects\Address;

Merchant::setClient($supplierClient); // Client con il token del Supplier, senza mTLS

$merchant = Merchant::create(new MerchantDraft(
    vatNumber: '12345678901',
    address: new Address(
        streetAddress: 'street name',
        streetNumber: 'xx',
        zipCode: '00000',
        city: 'City',
        province: 'XX',
    ),
    businessName: 'business name',
    email: 'merchant@example.com',
    password: 'MerchantP4$$w0rd',
));

2. Crea e attiva un PEM

use AndreaBallarin\ACubeItalyReceipts\Resources\Pem;

$pem = Pem::create($merchant->uuid); // ancora col client Supplier

Pem::setClient($merchantClient); // Client col token del Merchant, senza mTLS
Pem::activate($pem->serialNumber, $pem->registrationKey); // non ritorna nulla

3. Crea il cash register (rilascia il certificato mTLS)

use AndreaBallarin\ACubeItalyReceipts\Resources\CashRegister;

CashRegister::setClient($merchantClient);

$cashRegister = CashRegister::create($pem->serialNumber, 'Cash register first floor');

// mtlsCertificate e privateKey sono già "unescaped" (newline reali), pronti per il disco
file_put_contents('/var/certs/pems/A2F4-000001/cr-cert.pem', $cashRegister->mtlsCertificate);
file_put_contents('/var/certs/pems/A2F4-000001/cr-key.pem', $cashRegister->privateKey);

4. Recupera il dettaglio fiscale completo di uno scontrino

Nota: richiede lo stesso Client con mTLS

use AndreaBallarin\ACubeItalyReceipts\Resources\ReceiptDetails;

$details = ReceiptDetails::get($receipt->uuid);
echo "Imponibile: {$details->totalTaxableAmount}, IVA: {$details->totalVatAmount}";
foreach ($details->items as $item) {
    echo "{$item->description}: {$item->unitPrice}\n";
}

Login & Autenticazione

L'SDK include AuthenticationService per gestire il login presso Acube:

use AndreaBallarin\ACubeItalyReceipts\AuthenticationService;

$auth = new AuthenticationService(environment: Environment::Sandbox);
try {
    $token = $auth->login(email: 'user@example.com', password: 'secret');
    // Usa il token con il Client
} catch (AcubeAuthenticationException $e) {
    echo "Login failed: invalid credentials";
}

Note:

  • AuthenticationService chiama POST https://common-sandbox.api.acubeapi.com/login (endpoint separato)
  • Non richiede mTLS (diversamente da eReceipts)
  • Ritorna direttamente il JWT Bearer token
  • Il token è necessario per creare il Client che accede agli scontrini

Caching del JWT (TTL-aware)

Per i flussi sincroni in cassa, ogni login fresco aggiunge una round-trip di rete. Se emetti più scontrini rapidamente, il caching del JWT riusa il token finché è valido, riloggando solo quando sta per scadere:

use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Psr16Cache;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

// Qualsiasi implementazione PSR-16 va bene (Redis, Memcached, ArrayAdapter, ecc.)
$cache = new Psr16Cache(new ArrayAdapter());

$auth = new AuthenticationService(
    environment: Environment::Sandbox,
    cache: $cache,
    cacheLeewaySeconds: 30, // margine di sicurezza (default)
);

// Prima chiamata: fetcha da rete, cachea il token per la durata della sua validità
$token1 = $auth->loginCached(email: 'user@example.com', password: 'secret');

// Successive chiamate entro la scadenza: ritorna il token cachato (nessuna rete)
$token2 = $auth->loginCached(email: 'user@example.com', password: 'secret');
assert($token1 === $token2);

Dettagli:

  • loginCached() decodifica il claim exp dal JWT e cachea il token fino a quel momento, sottraendo un margine di sicurezza (cacheLeewaySeconds, default 30 secondi) per evitare token quasi-scaduti.
  • Se non inietti $cache, il comportamento è identico a login() (sempre rete, nessun caching).
  • La chiave di cache è un hash SHA-256 dell'email e dell'ambiente, quindi non espone l'email in chiaro nel backend di cache.
  • Email diverse usano chiavi di cache diverse.

Features

Punto 1: Dependency Injection

Due modalità di utilizzo:

  • Statica (Service Locator): Receipt::setClient($client) + Receipt::create(...)
  • Instance-based (DI-friendly): Passa il client al costruttore se la tua classe lo supporta

Punto 2: DecimalAmount Validation

Tutti gli importi/quantità sono string per preservare la precisione. DecimalAmount::assertValid() fallisce velocemente:

new ReceiptItem(
    quantity: '1.00',      // validato automaticamente
    unitPrice: '10.50',    // validato
);

Punto 3: UUID Validation

UuidValidator valida localmente prima di fare richieste HTTP:

Receipt::get('invalid-uuid'); // InvalidArgumentException locale, non 404 da Acube

Punto 4: PSR-3 Logging

Inietta un logger PSR-3 per registrare:

  • Richieste POST critiche (creazione documenti)
  • Errori API (status ≥400)
  • Errori di trasporto
$client = new Client(
    bearerToken: $token,
    logger: $myLogger, // Psr\Log\LoggerInterface
);

Punto 5: Dump delle chiamate HTTP

Per diagnosticare a posteriori un'emissione fallita (o qualunque altra chiamata), il Client può salvare un file JSON per ogni chiamata HTTP (richiesta + risposta complete). callDumpStorage accetta sia una cartella locale sia uno storage esterno (es. un disco Laravel):

// Cartella locale
$client = new Client(
    bearerToken: $token,
    callDumpStorage: __DIR__ . '/var/acube-calls', // opzionale
);

// Oppure un disco Laravel (o qualunque oggetto con put(string $path, string $contents)),
// con sottocartella dedicata dentro quel disco
$client = new Client(
    bearerToken: $token,
    callDumpStorage: \Illuminate\Support\Facades\Storage::disk('tmp'),
    callDumpPath: 'acube-calls', // opzionale: isola i dump in una sottocartella del disco
);

Dettagli:

  • Un file JSON per chiamata (timestamp, durata, richiesta e risposta complete, o l'errore in caso di fallimento di trasporto).
  • Authorization, password e registration_key sono sempre redatti prima della scrittura — nessun segreto finisce su disco in chiaro.
  • I fallimenti di scrittura del dump non bloccano mai la chiamata reale (vengono solo loggati via PSR-3 se un logger è iniettato).
  • Nessuna pulizia automatica: la cartella/disco cresce indefinitamente. La retention (cron, logrotate, lifecycle policy su S3, ecc.) è responsabilità dell'applicazione host.
  • Nessuna dipendenza da illuminate/filesystem aggiunta: la compatibilità con Storage::disk() è puro duck-typing sul metodo put().

Gestione Errori

use AndreaBallarin\ACubeItalyReceipts\Exceptions\{
    AcubeValidationException,
    AcubeNotFoundException,
    AcubeException,
};

try {
    $receipt = Receipt::create($draft);
} catch (AcubeValidationException $e) {
    foreach ($e->violations() as $field => $msg) {
        echo "$field: $msg";
    }
} catch (AcubeNotFoundException $e) {
    echo "Document not found";
} catch (AcubeException $e) {
    echo "API Error [{$e->problem->status}]: {$e->problem->detail}";
}

Idempotenza (Best-Effort, Client-Side)

$receipt = Receipt::create(
    draft: $draft,
    idempotencyKey: 'unique-key-123',
    idempotencyWindowSeconds: 60,
);
// Se ripeti la stessa chiave entro 60 secondi: AcubeDuplicateRequestException

Nota: Questa guardia protegge solo dentro lo stesso processo. Acube non offre idempotency key server-side, quindi un timeout di rete potrebbe causare duplicati.

Pattern consigliato per retry sicuri

Dopo un fallimento incerto (es. timeout di rete), verifica che lo scontrino non sia già stato emesso prima di riprare:

use AndreaBallarin\ACubeItalyReceipts\Resources\Receipt;

$pemSerialNumber = 'A2F4-000001';
$draft = new ReceiptDraft(items: [...], ...);

try {
    $receipt = Receipt::create($draft);
} catch (\Exception $e) {
    if (!isUncertainError($e)) {
        throw;
    }

    // Verifica se lo scontrino è già stato emesso (ultimi 2 minuti)
    $now = new \DateTime('now', new \DateTimeZone('UTC'));
    $twoMinutesAgo = (clone $now)->modify('-2 minutes')->format(\DateTimeInterface::ATOM);

    $recentReceipts = Receipt::listForPem(
        serialNumber: $pemSerialNumber,
        documentDatetimeAfter: $twoMinutesAgo,
        size: 100,
    );

    // Cerca uno scontrino con lo stesso importo totale
    foreach ($recentReceipts->data as $existing) {
        if ($existing->totalAmount === $draft->cashPaymentAmount) {
            return $existing; // Già emesso
        }
    }

    // Non trovato → riprova
    return Receipt::create($draft);
}

Test

composer test

Struttura

src/
  Client.php                    # Transport PSR-18, logging PSR-3, dump chiamate opzionale
  Environment.php               # Enum Sandbox|Production
  Model.php                     # Base per i dati
  ApiResource.php               # Base per le risorse (setClient/client)
  Internal/
    DecimalAmount.php           # Validazione importi
    UuidValidator.php           # Validazione UUID RFC 4122
    IdempotencyGuard.php        # Guardia client-side idempotenza
    JwtDecoder.php              # Decodifica exp da JWT (per TTL caching)
    CallDumper.php              # Dump JSON delle chiamate HTTP (debug), redazione segreti
    LocalCallDumpWriter.php     # Writer su cartella locale per CallDumper
  Exceptions/
    AcubeException.php          # Base astratta
    AcubeAuthenticationException.php    # 401
    AcubeValidationException.php        # 422
    ... (e altre per 403, 404, 409, 500, 503)
  ValueObjects/
    ProblemDetails.php          # RFC 7807
    Address.php                 # Indirizzo (merchant/PEM)
  Resources/
    Merchant.php                # Merchant registrato (MerchantOutput)
    MerchantDraft.php           # Payload di creazione (MerchantInput)
    Pem.php                     # PEM: create/get/activate
    CashRegister.php            # Cash register: create (rilascia mTLS)
    Receipt.php                 # Documento emesso (ReceiptOutput)
    ReceiptDraft.php            # Payload di creazione (ReceiptInput)
    ReceiptItem.php             # Riga di dettaglio
    ReceiptDetails.php          # Dettaglio fiscale completo (righe + totali)
    Enums/
      ReceiptType.php
      ReceiptStatus.php
      ReceiptItemType.php

Licenza

MIT

Autore

Andrea Ballarin ballarinandrea@icloud.com