Ugrás a tartalomra
kassza

Fizetett rendelés számlája

A vevő kifizette a rendelést, a fizetési szolgáltató (Stripe, Barion, SimplePay) szól a szerverednek, és neked számlát kell kiállítanod. A gond az, hogy a webhook többször is megérkezhet, a Számlázz.hu pedig időtúllépés után is elkészíthette a számlát. Ez a recept egy újrahasznosítható függvényt ad, amely mindkét esetben pontosan egy számlát hagy maga után.

A minta három lépésből áll:

  1. A rendelésszám a saját rendelésed azonosítójából készül, így mindig ugyanaz.
  2. Kiállítás előtt a kassza rendelésszám alapján megnézi, van-e már számla.
  3. Bizonytalan kimenetű hiba után újra megnézi, és ha a számla mégis elkészült, azt adja vissza.

A számlázó függvény#

lib/szamlazas.ts
import {
  type CreateInvoiceInput,
  type InvoiceBuyer,
  type InvoiceItemInput,
  isSzamlazzError,
  type Kassza,
  type SzamlazzErrorCategory,
} from 'kassza'
import { kassza as alapKliens } from '@/lib/kassza'

export type SzamlaRendelesszammal = CreateInvoiceInput & { readonly orderNumber: string }

export interface FizetettRendeles {
  readonly id: string
  readonly fizetesiMod: string
  readonly vevo: InvoiceBuyer
  readonly tetelek: readonly InvoiceItemInput[]
}

const BIZONYTALAN = new Set<SzamlazzErrorCategory>([
  'network',
  'timeout',
  'partial_success',
  'duplicate',
])

async function meglevoBizonylat(
  kliens: Kassza,
  adatok: SzamlaRendelesszammal,
): Promise<string | undefined> {
  const talalat = await kliens.invoices.find({ orderNumber: adatok.orderNumber })
  if (talalat && talalat.header.type === (adatok.type ?? 'invoice')) return talalat.header.number
  return undefined
}

export async function egyszerSzamlaz(
  adatok: SzamlaRendelesszammal,
  kliens: Kassza = alapKliens,
): Promise<string> {
  const meglevo = await meglevoBizonylat(kliens, adatok)
  if (meglevo) return meglevo

  try {
    const szamla = await kliens.invoices.create(adatok)
    return szamla.number
  } catch (error) {
    if (!isSzamlazzError(error) || !BIZONYTALAN.has(error.category)) throw error
    const letrejott = await meglevoBizonylat(kliens, adatok)
    if (letrejott) return letrejott
    throw error
  }
}

export function szamlazFizetettRendelest(
  rendeles: FizetettRendeles,
  kliens: Kassza = alapKliens,
): Promise<string> {
  return egyszerSzamlaz(
    {
      orderNumber: `WEB-${rendeles.id}`,
      paid: true,
      paymentMethod: rendeles.fizetesiMod,
      downloadPdf: false,
      buyer: rendeles.vevo,
      items: rendeles.tetelek,
    },
    kliens,
  )
}

Az egyszerSzamlaz() bármilyen bizonylattípussal működik, a nevezéses és a devizás recept is ezt használja. A második paraméterben kapott kliens miatt a függvény mock klienssel tesztelhető.

A tételeknél bruttó egységárat és áfakulcsot adj meg, az összegeket a kassza számolja ki a Számlázz.hu kerekítési szabályai szerint:

lib/rendeles-tetelek.ts
import type { InvoiceItemInput } from 'kassza'

export interface KosarSor {
  readonly termeknev: string
  readonly darab: number
  readonly bruttoEgysegar: number
}

export function szamlaTetelek(kosar: readonly KosarSor[]): InvoiceItemInput[] {
  return kosar.map((sor) => ({
    name: sor.termeknev,
    quantity: sor.darab,
    grossUnitPrice: sor.bruttoEgysegar,
    vat: 27,
  }))
}

Route handler#

Ezt a végpontot hívja a fizetési szolgáltató visszahívása után a saját kódod, vagy egy admin felület „Számla újrapróbálása” gombja. A @/lib/rendelesek a saját adatbázis-réteged: a rendelesBetoltese() a FizetettRendeles mezői mellett a fizetve és a szamlaszam mezőt is visszaadja.

app/api/rendelesek/[id]/szamla/route.ts
import { isSzamlazzError, type SzamlazzError } from 'kassza'
import { rendelesBetoltese, szamlaszamMentese } from '@/lib/rendelesek'
import { szamlazFizetettRendelest } from '@/lib/szamlazas'

function hibaStatusz(error: SzamlazzError): number {
  if (error.category === 'validation') return 422
  if (error.retryable) return 503
  return 502
}

export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
  const token = process.env.BELSO_API_TOKEN
  if (!token || request.headers.get('authorization') !== `Bearer ${token}`) {
    return new Response('Nincs jogosultság', { status: 401 })
  }

  const { id } = await params
  const rendeles = await rendelesBetoltese(id)
  if (!rendeles) return Response.json({ hiba: 'Nincs ilyen rendelés.' }, { status: 404 })
  if (!rendeles.fizetve) {
    return Response.json({ hiba: 'A rendelés még nincs kifizetve.' }, { status: 409 })
  }
  if (rendeles.szamlaszam) return Response.json({ szamlaszam: rendeles.szamlaszam })

  try {
    const szamlaszam = await szamlazFizetettRendelest(rendeles)
    await szamlaszamMentese(rendeles.id, szamlaszam)
    return Response.json({ szamlaszam })
  } catch (error) {
    if (!isSzamlazzError(error)) throw error
    return Response.json(
      { hiba: error.message, tipp: error.hint, kategoria: error.category },
      { status: hibaStatusz(error) },
    )
  }
}

A saját adatbázisban tárolt számlaszám az első védvonal: ha már megvan, a végpont a Számlázz.hu-t sem hívja. A rendelésszámos lekérdezés a második, arra az esetre, ha a számla elkészült, de a mentés már nem.

Idempotens számlázás hiba után

Részleges siker (56) után a find megtalálja a számlát, dupla számla nem készül.

Futtatás a sandboxban
hibakezeles-idempotens.ts
import { ,  } from 'kassza'
import {  } from 'kassza-sandbox'

const  = ()

interface Rendeles {
  readonly : number
  readonly : string
  readonly : string
  readonly : number
}

async function (: Rendeles): <string> {
  const  = `REND-${.}`

  const  = await ..({  })
  if () return `${..} (már létezett)`

  try {
    const  = await ..({
      ,
      : true,
      : 'bankkártya',
      : {
        : .,
        : '1111',
        : 'Budapest',
        : 'Fő utca 1.',
        : .,
      },
      : [{ : 'Rendelés', : ., : 27 }],
    })
    return `${.} (új)`
  } catch () {
    const  = ['network', 'timeout', 'partial_success', 'duplicate']
    if (() && .(.)) {
      .(`Bizonytalan kimenet (${.}), ellenőrzés rendelésszám alapján…`)
      const  = await ..({  })
      if () return `${..} (a hiba ellenére elkészült)`
    }
    throw 
  }
}

const : Rendeles = {
  : 5001,
  : 'Nagy Péter',
  : 'peter@example.hu',
  : 12_700,
}

.failNext('createInvoice', 56)
.('Első próbálkozás:', await ())
.('A webhook újraküldése:', await ())
.('Számlák a fiókban:', .account().invoices.length)

Buktatók#

Kapcsolódó#

Oldal szerkesztéseUtoljára frissítve: