Ugrás a tartalomra
kassza

Serverless és edge

A kasszának nincs futásidejű függősége, és csak webes szabvány API-kat használ (fetch, FormData, AbortSignal, Web Crypto). Emiatt Node.js 22-n és újabbon, Bunon, Denón, Cloudflare Workersen és a Vercel Edge futtatókörnyezetében is ugyanaz a kód fut. Egyetlen kivétel a kassza/storage/fs, amely a node:fs modult használja.

Környezettől függetlenül három dolgot kell eldöntened:

  1. Hol él a kliens? Folyamatonként vagy izolátumonként egyszer hozd létre, modul szinten, ne kérésenként.
  2. Hol él a munkamenet? Ha a példányok nem osztoznak a memórián, adj meg közös cookieStore-t. A részleteket a Munkamenet oldal írja le.
  3. Mennyi ideig futhat egy kérés? A kassza alapból 60 másodpercig vár egy válaszra, ami több lehet, mint amennyit a platform vagy a webhook küldője enged.
KörnyezetAgent kulcsMunkamenetPDF tárhely
Node.js, Bun, Deno szerverenprocess.env, Denón Deno.env.get()memória (alapértelmezett)fsStorage, S3
Vercel Functionsprocess.envUpstash, ioredis, node-redisVercel Blob, S3, R2
Vercel Edgeprocess.envUpstashs3FetchStorage
Cloudflare Workersenv.SZAMLAZZ_AGENT_KEYCloudflare KVr2BindingStorage
AWS Lambdaprocess.envioredis, node-redis, UpstashS3

Edge környezetben HTTP alapú tárolót válassz (Upstash Redis, Cloudflare KV), mert az ioredis és a node-redis TCP kapcsolattal dolgozik.

Platformonként#

A klienst egy közös modulban hozd létre. A Vercel Functions példányai nem osztoznak a memórián, ezért a munkamenetet Upstash Redisben tárold:

lib/kassza.ts
import { Redis } from '@upstash/redis'
import { createKassza } from 'kassza'
import { upstashRedisCookieStore } from 'kassza/cookie-stores'

export const kassza = createKassza({
  cookieStore: upstashRedisCookieStore(Redis.fromEnv()),
  timeoutMs: 15_000,
  maxAttempts: 2,
})

Ugyanez a modul Node.js és Edge futtatókörnyezetben is működik:

app/api/szamlazas/allapot/route.ts
import { kassza } from '@/lib/kassza'

export const runtime = 'edge'

export async function GET() {
  const kulcsJo = await kassza.verifyCredentials()
  return Response.json({ kulcsJo }, { status: kulcsJo ? 200 : 500 })
}

Az Agent kulcsot a projekt beállításaiban vagy a vercel env add SZAMLAZZ_AGENT_KEY paranccsal add meg. Soha ne adj neki NEXT_PUBLIC_ előtagot, mert az ilyen változók a böngészőbe kerülnek.

Környezeti változók#

  • A kassza a SZAMLAZZ_AGENT_KEY változót a process.env-ből olvassa. Ahol ez nincs (Cloudflare Workers, Deno), add át a kulcsot az agentKey opcióban.
  • A createKassza() már létrehozáskor configuration hibát dob, ha nincs kulcs, vagy ha a kulcs nagybetűt tartalmaz. Ha a klienst modul szinten hozod létre, a hiba már az első importnál jelentkezik, ezért a változót minden környezetben állítsd be, ahol a kód fut.
  • Fejlesztéshez és az előnézeti környezetekhez Számlázz.hu tesztfiók kulcsát használd, az éles kulcs csak az éles környezetbe kerüljön.
  • Telepítés után a verifyCredentials() megmondja, hogy a kulcs jó-e, bizonylat létrehozása nélkül. Hibás kulcsnál false, fiókproblémánál hibát dob.

Időkorlát#

A serverless platformok és a webhookot küldő szolgáltatók is korlátozzák, meddig futhat egy kérés. Vercelen ezt a route maxDuration beállítása, Lambdán a függvény időkorlátja adja meg. A kassza oldalán két beállítás számít:

  • timeoutMs: egy próbálkozás időkorlátja, alapból 60 000 ms. Túllépésnél timeout kategóriájú SzamlazzError jön.
  • maxAttempts: a biztonságosan ismételhető műveletek (lekérdezések, befizetések felülírása, callId-val védett nyugták) próbálkozásainak száma hálózati hiba, időtúllépés vagy karbantartás esetén, alapból 3. Két próbálkozás között 1, majd 2 másodperc a várakozás.

Egy lekérdezés így legrosszabb esetben maxAttempts × timeoutMs plusz a várakozások ideig tarthat. A számlakészítést a kassza soha nem ismétli, az legfeljebb egy timeoutMs-ig tart.

Beállításinvoices.create legfeljebbinvoices.find legfeljebb
Alapértelmezés (timeoutMs: 60_000, maxAttempts: 3)60 s183 s
timeoutMs: 15_000, maxAttempts: 215 s31 s

Ha egy kérésnek összesen van határideje, adj át egy közös AbortSignal-t minden hívásnak. A signal a próbálkozásokat és a köztük lévő várakozást is megszakítja.

Webhook minták#

Fizetés után számla#

A fizetési szolgáltatók (Stripe, Barion, SimplePay és társaik) ugyanazt az eseményt többször is elküldhetik, és újraküldik, ha nem kapnak időben sikeres választ. Ezt kihasználva a webhook bizonytalan kimenetnél nem próbálkozik tovább, hanem hibakóddal válaszol, és a következő kézbesítés az elején rendelésszám alapján megtalálja a közben elkészült számlát:

app/api/fizetes/webhook/route.ts
import { isSzamlazzError } from 'kassza'
import { kassza } from '@/lib/kassza'

const BIZONYTALAN = ['network', 'timeout', 'partial_success', 'duplicate']

function bizonytalan(error: unknown): boolean {
  if (isSzamlazzError(error)) return BIZONYTALAN.includes(error.category)
  return error instanceof DOMException && error.name === 'TimeoutError'
}

export async function POST(request: Request) {
  const esemeny = await ellenorzottEsemeny(request)
  const orderNumber = `REND-${esemeny.rendelesId}`
  const signal = AbortSignal.timeout(8_000)

  try {
    const meglevo = await kassza.invoices.find({ orderNumber }, {}, { signal })
    if (meglevo) return Response.json({ szamla: meglevo.header.number })

    const szamla = await kassza.invoices.create(szamlaAdatok(esemeny, orderNumber), { signal })
    return Response.json({ szamla: szamla.number })
  } catch (error) {
    if (bizonytalan(error)) return new Response('Próbáld újra később', { status: 503 })
    throw error
  }
}
  • Az ellenorzottEsemeny a szolgáltató aláírását ellenőrzi, a szamlaAdatok a rendelésből állítja össze a számlát.
  • A rendelésszám a rendelés azonosítójából készül, így minden kézbesítés ugyanazt a számlát keresi.
  • A find az első lépés, ezért a webhook tetszőleges számú ismétlés mellett is legfeljebb egy számlát állít ki.

A hibakódok jelentését és a bizonytalan kimenetek kezelését a Hibakezelés oldal írja le.

Fizetési értesítés a Számlázz.hu-tól (IPN)#

Az IPN-t a Számlázz.hu küldi, ha egy számla kifizetett összege megváltozik. Sikertelen fogadásnál 3 percenként újraküldi, legfeljebb tízszer. Válaszolj gyorsan 200-zal, és a feldolgozás legyen idempotens:

app/api/szamlazz/ipn/route.ts
import { ipnOkResponse, readIpnNotification } from 'kassza/ipn'

export async function POST(request: Request) {
  const ertesites = await readIpnNotification(request)
  if (ertesites.isFullyPaid) await rendelesFizetve(ertesites.invoiceNumber)
  return ipnOkResponse()
}

A readIpnNotification() szabványos Request-et vár, így Next.js route handlerben, Workersben, Denóban és Bunban is ugyanígy működik. Ahol csak a nyers törzs van meg, például AWS Lambdán, a parseIpnNotification() a szöveges törzset is feldolgozza. A forrás IP-cím ellenőrzését (isSzamlazzIp()) és a proxyval kapcsolatos buktatókat a Hálózat és biztonság oldal írja le.

Hosszú munka sorban#

Ha a webhookra néhány másodpercen belül válaszolni kell, a számlázást ne a webhook végezze. A webhook tegye az eseményt egy üzenetsorba (például Cloudflare Queues vagy Amazon SQS), és a feldolgozó függvény állítsa ki a számlát, mentse a PDF-et, és küldje ki az e-mailt. A feldolgozó is rendelésszám alapján keressen először, mert a sorok is kézbesíthetnek egy üzenetet többször.

PDF mentése#

Serverless függvényben a fájlrendszer nem tartós, ezért a PDF-et objektumtárba mentsd: S3-ba vagy R2-be az s3FetchStorage-dzsal, Workersben az r2BindingStorage-dzsal, Vercelen a vercelBlobStorage-dzsal. A részletek a PDF tárhely oldalon vannak.

Oldal szerkesztéseUtoljára frissítve: