Ugrás a tartalomra
kassza

Pontosan egyszer

Egy webhook, egy várólista vagy egy türelmetlen felhasználó ugyanazt a kérést többször is elküldheti. A Számlázz.hu ugyanakkor nem engedi, hogy egy sikertelen kérést ötnél többször elküldj, és egy időtúllépés után nem tudhatod, elkészült-e a bizonylat. Ezt a három problémát oldják meg az alábbi eszközök.

createOnce: lekérdezés, kiállítás, visszakeresés#

Az invoices.createOnce() és a receipts.createOnce() ugyanazt csinálja, amit a Hibakezelés oldal kézzel leír:

  1. megkeresi a bizonylatot, és ha már létezik, azt adja vissza,
  2. ha nem létezik, kiállítja,
  3. ha a kiállítás kimenete bizonytalan (hálózati hiba, időtúllépés, 56-os vagy dupla hiba), rövid várakozás után kétszer újra megkeresi.
const eredmeny = await kassza.invoices.createOnce({
  orderNumber: 'WEB-1042',
  paid: true,
  buyer: { name: 'Vevő Kft.', zip: '1111', city: 'Budapest', address: 'Fő utca 1.' },
  items: [{ name: 'Előfizetés', grossUnitPrice: 12_700, vat: 27 }],
})

console.log(eredmeny.number, eredmeny.created)
MezőJelentés
numberA számla száma, akár most készült, akár korábban.
createdtrue, ha ez a hívás állította ki.
externalIdA külső azonosító, amivel a számlát keresi.
invoiceA kiállítás válasza, ha ez a hívás állította ki.
detailsA lekérdezett számla, ha már létezett.

A rendelésszám kötelező. A külső azonosító alapból a rendelésszám, a bizonylattípus jelével: díjbekérőnél /D, előlegszámlánál /E, végszámlánál /V, helyesbítőnél /H, szállítólevélnél /SZL. Így egy rendeléshez tartozó díjbekérő és számla nem keveredik össze. Saját externalId is megadható.

Nyugtánál a receipts.createOnce() a rendelésszámot hívásazonosítóként (callId) is használja, ha nem adsz meg mást. A visszatérési értéke { receipt, created }.

OpcióAlapértékJelentés
lookupFirsttrueKiállítás előtt megkeresi a bizonylatot.
matchOrderNumbertrueSzámlánál a külső azonosító mellett a rendelésszámra is keres.
recoveryDelayMs1000A visszakeresések előtti várakozás (az első után duplázódik).
signal–Megszakítás.
locka createOnceLockHívásonkénti elosztott zár, vagy false a kikapcsoláshoz.
lockTtlSeconds600Meddig él a zár, ha a folyamat menet közben leáll.
lockWaitMs10000Meddig vár egy másik folyamat zárjára, mielőtt in_progress hibát ad.
lockFailure'throw'Mi legyen, ha a zár tárolója nem érhető el: 'throw' vagy 'proceed'.

Egyidejű hívások: egy rendelés, egy bizonylat#

Ugyanaz a webhook kétszer is megérkezhet egyszerre, vagy két serverless példány dolgozhatja fel ugyanazt a rendelést. Ha két createOnce() hívás egyszerre keres, mindkettő azt látja, hogy még nincs számla, és mindkettő kiállítaná. Nyugtánál a Számlázz.hu a második kérést a hívásazonosító miatt elutasítja, számlánál viszont csak akkor, ha a fiókban be van kapcsolva a rendelésszám ismétlődésének tiltása.

A kassza ezért két szinten véd:

  1. Egy folyamaton belül az azonos rendelésre egyszerre érkező hívások összevonódnak: az első kiállít, a többi ugyanazt a bizonylatot kapja created: false jelzéssel. Ez akkor is működik, ha kérésenként új kassza példányt hozol létre (serverless minta). Nem kell hozzá beállítás.
  2. Folyamatok között egy közös tárolóban tartott, rövid életű zár véd. A zárat az kapja meg, aki elsőként kéri; a többi folyamat megvárja, majd visszakeresi a már elkészült bizonylatot.
lib/kassza.ts
import { Redis } from '@upstash/redis'
import { createKassza } from 'kassza'
import { upstashRedisStore } from 'kassza/stores'

const store = upstashRedisStore(Redis.fromEnv())

export const kassza = createKassza({
  cookieStore: store,
  attemptLedger: store,
  createOnceLock: store,
})

A zárhoz olyan tároló kell, amely atomikusan tud „írni, ha még nincs” (setIfAbsent): a kassza/stores Redis, Upstash és Durable Object tárolója ilyen, a Cloudflare KV nem. Ha egy másik folyamat a várakozási időn belül nem végez, a kassza in_progress kategóriájú hibát dob: ilyenkor ne állítsd ki kézzel, hanem hívd meg később ugyanígy a createOnce()-t (webhookban adj vissza 5xx választ, és a szolgáltató újraküldi).

Kitiltás elleni védelem folyamatok között#

A Számlázz.hu egy kérést legfeljebb ötször enged elküldeni. A kassza egy folyamaton belül ezt magától betartja, de ha ugyanazt a kérést több szerver, több serverless példány vagy egy újrapróbáló várólista küldi, a folyamatok nem tudnak egymásról. Erre való az attemptLedger opció: egy közös tároló, amelyben a kassza számolja az azonos kérések sikertelen próbálkozásait.

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

const store = upstashRedisStore(Redis.fromEnv())

export const kassza = createKassza({
  cookieStore: store,
  attemptLedger: store,
})

A tároló ugyanaz a felület, mint a munkamenet tárolója, így bármelyik kassza/stores adapter megfelel (a régi kassza/cookie-stores útvonal is működik). A kulcsot a kérés tartalmából képzi a kassza (SHA-256), az Agent kulcsot nem tárolja, és egy napig őrzi. Ha a tároló tud atomikus számlálót (increment, a Redis és Upstash adapterek igen), két egyszerre hibázó folyamat sem veszít el próbálkozást.

Ha a tároló nem érhető el, a kassza alapból továbbengedi a kérést, és a folyamaton belüli számlálóval véd (attemptLedgerMode: 'fail-open'), a hibát pedig az onWarning hooknak jelzi. Ha inkább ne menjen ki kérés, amíg a napló nem működik, állítsd 'fail-closed'-ra: ilyenkor store_unavailable hibát kapsz. Ehhez a tárolót resilient: false beállítással hozd létre, különben az adapter maga elnyeli az olvasási hibát.

Öt sikertelen próbálkozás után a kassza már el sem küldi a kérést, hanem attempt_limit kategóriájú hibát dob. Ilyenkor embernek kell ránéznie. A hiba elhárítása után a számlálót a hibával törölheted:

import { isSzamlazzError } from 'kassza'

try {
  await kassza.invoices.create(szamla)
} catch (error) {
  if (isSzamlazzError(error) && error.category === 'attempt_limit') {
    await riasztas(error.message)
    await kassza.resetAttempts(error)
  }
  throw error
}

Sikeres válasz után a számláló magától törlődik. A próbálkozások közé csak a kérésből eredő hibák számítanak (például validáció, fiókhiba, hálózati hiba); a dupla bizonylat és a nem található bizonylat nem.

Karbantartási szünet#

Karbantartás (1-es hiba) idején a Számlázz.hu percekig nem érhető el. A maintenanceCooldownMs opcióval a kassza az első karbantartási hiba után a megadott ideig el sem küldi a kéréseket, hanem azonnal maintenance hibát ad. Így egy várólista nem égeti el a próbálkozásait, a pénztár pedig gyorsan átválthat tartalék megoldásra.

const kassza = createKassza({ maintenanceCooldownMs: 120_000 })

onDocument: minden elkészült bizonylatról értesítés#

Az onDocument hook minden sikeres kiállítás, sztornó és befizetés után lefut, a bizonylat adataival. Jó hely a könyvelési napló, az e-mail sor vagy a statisztika frissítésére. Ha a hook hibát dob, a bizonylat attól még elkészült. Hogy ilyenkor mi történjen, az onDocumentError dönti el:

onDocumentErrorViselkedés
'warn' (alap)Figyelmeztet (onWarning, vagy ha nincs, a konzolra), és a hívás sikeresen visszatér.
'throw'DocumentHookError-t dob, amely tartalmazza a már elkészült bizonylatot (error.event, error.document, error.number).
függvényA te kezelőd kapja meg a hibát és az eseményt; ha az is dob, DocumentHookError lesz belőle.

Ha a hook egy jogilag fontos naplót ír (például a NAV napi összesítőhöz használt bizonylatnaplót), használd a 'throw' módot, hogy egy kimaradt bejegyzés ne maradjon észrevétlen. A hibából a bizonylat száma kiolvasható: ne állítsd ki újra, csak a naplóba írást pótold.

const kassza = createKassza({
  hooks: {
    onDocument: async (esemeny) => {
      await naplo.mentes({
        fajta: esemeny.kind,
        muvelet: esemeny.action,
        szam: esemeny.number,
      })
    },
  },
})
kindactionMikor
invoicecreatedSzámla, díjbekérő, előleg-, vég- vagy helyesbítő számla elkészült.
invoicereversedSztornó számla készült; a reversedNumber az eredeti szám.
invoicepaymentBefizetés rögzítése vagy törlése.
receiptcreatedNyugta készült.
receiptreversedSztornónyugta készült; a reversedNumber az eredeti szám.

onWarning: a korábban csendes hibák#

Néhány hibát a kassza szándékosan elnyel, hogy a számlázás ne akadjon el: a munkamenet tárolójának kiesését, a próbálkozás-napló hibáját, a hookok kivételeit, a zár felszabadításának hibáját. Az onWarning hook ezeket mind megkapja, így naplózhatod vagy riaszthatsz rájuk:

const kassza = createKassza({
  hooks: {
    onWarning: (figyelmeztetes) => {
      logger.warn({ fajta: figyelmeztetes.kind, muvelet: figyelmeztetes.operation }, figyelmeztetes.message)
    },
  },
})

A kind értéke session, ledger, hook, document, lock, dedupe vagy cache. A megfigyelhetőség modul ezeket metrikaként is számolja.

Kapcsolódó#

Oldal szerkesztéseUtoljára frissítve: