Ugrás a tartalomra
kassza

PDF tárhely

Számla kiállításakor a kassza alapból letölti a PDF-et is, és a válasz pdf mezőjében Uint8Array-ként adja vissza. Ha ezt elmented egy tárhelyre, a vevőnek letöltési linket adhatsz, és nem kell minden letöltésnél a Számlázz.hu-t hívnod. A kassza/storage modul ehhez ad egy közös felületet és kész adaptereket. Egyiknek sincs függősége: a szolgáltató SDK-ját, ha kell, te adod át.

lib/tarhely.ts
import { s3FetchStorage } from 'kassza/storage'

export const tarhely = s3FetchStorage({
  bucket: 'szamlak',
  region: 'auto',
  endpoint: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  accessKeyId: process.env.R2_ACCESS_KEY_ID ?? '',
  secretAccessKey: process.env.R2_SECRET_ACCESS_KEY ?? '',
})
import { invoicePdfKey, storePdf } from 'kassza/storage'
import { tarhely } from '@/lib/tarhely'

const szamla = await kassza.invoices.create(adatok)

if (szamla.pdf) {
  const fajl = await storePdf(tarhely, invoicePdfKey({ number: szamla.number }), szamla.pdf)
  await db.rendeles.update({ where: { id: rendelesId }, data: { szamlaPdfKulcs: fajl.key } })
}

A kulcs ebben a példában szamlak/2026/09/E-WEB-2026-12.pdf alakú lesz. Az adatbázisba a kulcsot mentsd, ne a PDF-et base64-ként és ne egy lejáró URL-t.

PDF mentése tárhelyre

A számla PDF-je dátum szerinti kulccsal egy memóriás tárhelyre.

Futtatás a sandboxban
pdf-tarhely.ts
import {  } from 'kassza'
import { , ,  } from 'kassza/storage'

const  = ()
const  = ({ : 'https://cdn.example.hu' })

const  = await ..({
  : 'REND-6001',
  : { : 'Vevő Kft.', : '1111', : 'Budapest', : 'Fő utca 1.' },
  : [{ : 'Fotózás', : 85_000, : 27 }],
})

if (.) {
  const  = await (, ({ : . }), .)
  .()
  .('Letöltési cím:', await .(.))
}

A storePdf#

A storePdf(storage, key, pdf) ellenőrzi, hogy a tartalom valóban PDF-e (a %PDF fejléccel kezdődik), majd application/pdf típussal elmenti. Ha a tartalom nem PDF, StorageError-t dob, és nem ír semmit. Az eredmény egy StoredFile:

MezőTípusLeírás
keystringAz általad megadott kulcs, az adapter prefix opciója nélkül.
urlstring | undefinedLetöltési cím, ha az adapter a mentéskor meg tudja adni.
sizenumberA fájl mérete bájtban.
contentTypestringapplication/pdf

Ha a számlát downloadPdf: false beállítással állítottad ki, vagy a mentés nem sikerült, a PDF-et később az invoices.getPdf() metódussal kérheted le.

Kulcsok#

Az invoicePdfKey() és a receiptPdfKey() év és hónap szerinti mappába rendezett, ékezet nélküli kulcsot készít a bizonylatszámból:

import { invoicePdfKey, receiptPdfKey } from 'kassza/storage'

const szamla = invoicePdfKey({ number: 'E-WEB-2026-12' })
const dijbekero = invoicePdfKey({ number: 'D-WEB-2026-3', type: 'proforma', date: '2026-01-31' })
const nyugta = receiptPdfKey({ number: 'NYGT-2026-45', date: '2026-09-17' })
const ugyfelenkent = invoicePdfKey({ number: 'E-WEB-2026-12', prefix: 'ugyfelek/Árvíztűrő Kft', date: '2026-09-17' })
VáltozóKulcs
szamlaszamlak/2026/09/E-WEB-2026-12.pdf (a mai nap szerint)
dijbekeroszamlak/dijbekero/2026/01/D-WEB-2026-3.pdf
nyugtanyugtak/2026/09/NYGT-2026-45.pdf
ugyfelenkentugyfelek/Arvizturo-Kft/2026/09/E-WEB-2026-12.pdf
ParaméterAlapértékLeírás
numberA bizonylatszám, ebből lesz a fájlnév. Kötelező.
type'invoice'Csak az invoicePdfKey-nél. Almappát ad: proformadijbekero, advanceelolegszamla, finalvegszamla, correctivehelyesbito, stornosztorno, deliveryNoteszallitolevel.
prefixszamlak vagy nyugtakAz első mappa, perjellel tagolva több szint is lehet.
datema, budapesti idő szerintDate vagy YYYY-MM-DD. Ebből lesz az év és a hónap.

Minden szakaszból eltűnnek az ékezetek, és ami nem betű, szám, pont, aláhúzás vagy kötőjel, az kötőjel lesz. Ugyanezt a sanitizeKeySegment() függvény külön is elvégzi.

Az adapterek minden kulcsot ellenőriznek: nem lehet üres vagy 1024 karakternél hosszabb, nem kezdődhet perjellel, és nem lehet benne visszaperjel, vezérlőkarakter, üres, . vagy .. szakasz. Hibás kulcsnál StorageError jön operation: 'key' értékkel. Saját adapterben ugyanezt az assertStorageKey() végzi.

Minden adapternek van prefix opciója. Ez a tárhelyen a kulcs elé kerül, például prefix: 'eles' mellett a fájl az eles/szamlak/2026/09/… útvonalra kerül, a StoredFile.key viszont prefix nélkül marad. Így ugyanazzal a kulccsal dolgozhatsz fejlesztői és éles tárhelyen is.

Adapterek#

Minden adapter tud menteni (put). A többi művelet adapterenként eltér:

AdapterImportgetdeletegetUrl lejárat nélkülgetUrl lejáró linkkel
s3FetchStoragekassza/storageigenigenpublicBaseUrl eseténaláírt URL, legfeljebb 7 nap
s3Storagekassza/storageGetObjectCommand-dalDeleteObjectCommand-dalpublicBaseUrl eseténgetSignedUrl-lel, legfeljebb 7 nap
r2BindingStoragekassza/storageigenigenpublicBaseUrl eseténnem
vercelBlobStoragekassza/storagenemdel-lelhead-delnem
uploadthingStoragekassza/storagenemigennem, mindig aláírtaláírt URL, legfeljebb 7 nap
supabaseStoragekassza/storageigenigenpublic: true eseténaláírt URL, legfeljebb 1 év
fsStoragekassza/storage/fsigenigenpublicBaseUrl eseténnem
memoryStoragekassza/storageigenigenmindignem

Ahol van aláírt URL, a lejárat alapból 1 óra, és a megengedettnél hosszabb lejárat StorageError-t ad. Az R2 binding, a Vercel Blob és a fájlrendszer adapternél már az expiresInSeconds megadása is StorageError-t ad.

Aláírt S3 kéréseket küld a beépített fetch-csel, AWS SDK nélkül. Az aláíráshoz a Web Crypto API-t használja, ezért edge környezetben is fut. Amazon S3, Cloudflare R2, MinIO, Backblaze és más S3-kompatibilis tárhelyek mellett is működik.

import { s3FetchStorage } from 'kassza/storage'

export const tarhely = s3FetchStorage({
  bucket: 'szamlak',
  region: 'eu-central-1',
  accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
})
OpcióLeírás
bucket, region, accessKeyId, secretAccessKeyKötelező. R2-nél a régió auto. Ha bármelyik üres, a létrehozás configuration hibát dob.
sessionTokenIdeiglenes AWS hitelesítéshez.
endpointNem AWS tárhelynél, például https://<account-id>.r2.cloudflarestorage.com.
forcePathStyleEndpoint nélkül alapból https://<bucket>.s3.<region>.amazonaws.com a cím, pontot tartalmazó bucketnél vagy true esetén https://s3.<region>.amazonaws.com/<bucket>. Endpointtal alapból <endpoint>/<bucket>, false esetén <bucket>.<endpoint>.
publicBaseUrlNyilvános cím, például egy CDN vagy az R2 egyedi domainje.
prefix, fetchKulcselőtag és saját fetch implementáció.

A vevő a PDF-et egy saját végponton keresztül kapja meg: a végpont ellenőrzi, hogy a bejelentkezett felhasználó láthatja-e a számlát, és rövid lejáratú linkre irányít át.

app/api/szamlak/[rendelesId]/route.ts
import { tarhely } from '@/lib/tarhely'

export async function GET(_request: Request, { params }: { params: Promise<{ rendelesId: string }> }) {
  const { rendelesId } = await params
  const rendeles = await db.rendeles.findUnique({ where: { id: rendelesId } })
  if (!rendeles?.szamlaPdfKulcs) return new Response('Nem található', { status: 404 })

  const url = await tarhely.getUrl(rendeles.szamlaPdfKulcs, { expiresInSeconds: 300 })
  return Response.redirect(url, 302)
}

Hibakezelés#

A tárhely hibáit a kassza StorageError-ba csomagolja:

MezőLeírás
messageMagyar hibaüzenet, benne a szolgáltató hibájával.
operation'put', 'get', 'delete', 'getUrl' vagy 'key'.
keyAz érintett kulcs.
statusHTTP státusz, ha ismert.
causeAz eredeti hiba.

Az isStorageError() típusőrrel szűkítheted. A hiányzó fájl nem hiba: a get() ilyenkor undefined-ot ad. A hiányzó kötelező opció (például a bucket vagy a directory) viszont már az adapter létrehozásakor configuration kategóriájú SzamlazzError-t dob.

A számla a mentés előtt már elkészült, ezért tárhelyhiba miatt soha ne állítsd ki újra. Naplózd a hibát, és a PDF-et később a számlaszám alapján kérd le újra:

import { invoicePdfKey, isStorageError, storePdf } from 'kassza/storage'
import { tarhely } from '@/lib/tarhely'

const szamla = await kassza.invoices.create(adatok)

try {
  if (szamla.pdf) await storePdf(tarhely, invoicePdfKey({ number: szamla.number }), szamla.pdf)
} catch (error) {
  if (!isStorageError(error)) throw error
  naplo.warn({ muvelet: error.operation, kulcs: error.key, status: error.status })
  await pdfMentesKesobb(szamla.number)
}

A pdfMentesKesobb egy háttérfeladatban a kassza.invoices.getPdf(szamlaszam) hívással tölti le újra a PDF-et, és megismétli a mentést.

Tesztelés#

A memoryStorage és a mock kliens együtt hálózat nélkül teszteli a teljes folyamatot. A mock minimális, de érvényes PDF-et ad vissza, amit a storePdf elfogad:

import { invoicePdfKey, memoryStorage, storePdf } from 'kassza/storage'
import { createMockKassza } from 'kassza/testing'
import { expect, test } from 'vitest'

test('a számla PDF-je a tárhelyre kerül', async () => {
  const kassza = createMockKassza({ now: () => new Date('2026-09-17T10:00:00Z') })
  const tarhely = memoryStorage()

  const szamla = await kassza.invoices.create({
    buyer: { name: 'Vevő Kft.', zip: '1111', city: 'Budapest', address: 'Fő utca 1.' },
    items: [{ name: 'Fotózás', netUnitPrice: 85_000, vat: 27 }],
  })
  const kulcs = invoicePdfKey({ number: szamla.number, date: '2026-09-17' })
  const fajl = await storePdf(tarhely, kulcs, szamla.pdf ?? new Uint8Array())

  expect(fajl.key).toBe('szamlak/2026/09/E-KASSZA-2026-1.pdf')
  expect(tarhely.files.get(fajl.key)?.contentType).toBe('application/pdf')
})

Saját adapter#

Ha a tárhelyedhez nincs adapter, írd meg a StorageAdapter felületet. Csak a put kötelező, a get, a delete és a getUrl opcionális. A guardStorageCall() a szolgáltató hibáját StorageError-ba csomagolja:

import { Storage } from '@google-cloud/storage'
import { assertStorageKey, guardStorageCall, type StorageAdapter } from 'kassza/storage'

const bucket = new Storage().bucket('szamlak')

export const tarhely: StorageAdapter = {
  async put(key, body, { contentType }) {
    const file = bucket.file(assertStorageKey(key))
    await guardStorageCall('Google Cloud Storage', 'put', key, () => file.save(body, { contentType }))
    return { key, size: body.byteLength, contentType }
  },
}
Oldal szerkesztéseUtoljára frissítve: