andreaballarin / acube-italy-receipts
Acube Italy Receipts Library
Package info
github.com/andreaballarin/acube-italy-receipts
pkg:composer/andreaballarin/acube-italy-receipts
Requires
- php: ^8.3
- guzzlehttp/guzzle: ^7.8
- guzzlehttp/psr7: ^2.6
- psr/http-client: ^1.0
- psr/http-factory: ^1.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^3.0
- psr/simple-cache: ^2.0 || ^3.0
Requires (Dev)
- phpunit/phpunit: ^10.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
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:
AuthenticationServicechiamaPOST 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
Clientche 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 claimexpdal 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 alogin()(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,passworderegistration_keysono 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/filesystemaggiunta: la compatibilità conStorage::disk()è puro duck-typing sul metodoput().
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