Ugrás a tartalomra
kassza

Nevezés díjbekérővel

Egy futóversenyre a jelentkezők egy űrlapon neveznek, a nevezési díjat átutalással fizetik. Nevezéskor díjbekérőt küldesz, amelyből a jelentkező látja, mennyit és meddig kell fizetnie. Amikor a pénz megérkezik, a díjbekérőre hivatkozó számlát állítasz ki. Ha valaki lemondja a nevezést, mielőtt fizetett, a díjbekérőt törlöd.

A díjbekérő és a számla ugyanazt a rendelésszámot kapja, így a kassza mindkettőt meg tudja keresni. A recept a Fizetett rendelés számlája recept egyszerSzamlaz() függvényére épül.

A nevezési logika#

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

export interface Nevezes {
  readonly id: string
  readonly vevo: InvoiceBuyer
  readonly tav: string
  readonly nevezesiDij: number
}

export type TorlesEredmenye = 'torolve' | 'nem-talalhato' | 'mar-szamlazva'

function rendelesszam(nevezes: Nevezes): string {
  return `NEV-${nevezes.id}`
}

function tetelek(nevezes: Nevezes): InvoiceItemInput[] {
  return [{ name: `Nevezési díj, ${nevezes.tav}`, grossUnitPrice: nevezes.nevezesiDij, vat: 27 }]
}

export function dijbekeroKiallitasa(
  nevezes: Nevezes,
  kliens: Kassza = alapKliens,
): Promise<string> {
  return egyszerSzamlaz(
    {
      type: 'proforma',
      orderNumber: rendelesszam(nevezes),
      paymentMethod: 'átutalás',
      paymentDueInDays: 5,
      downloadPdf: false,
      buyer: nevezes.vevo,
      items: tetelek(nevezes),
    },
    kliens,
  )
}

export async function szamlaBefizetesUtan(
  nevezes: Nevezes,
  kliens: Kassza = alapKliens,
): Promise<string> {
  const orderNumber = rendelesszam(nevezes)
  const legutobbi = await kliens.invoices.find({ orderNumber })
  if (legutobbi?.header.type === 'invoice') return legutobbi.header.number

  return egyszerSzamlaz(
    {
      orderNumber,
      proformaNumber: legutobbi?.header.type === 'proforma' ? legutobbi.header.number : undefined,
      paid: true,
      paymentMethod: 'átutalás',
      buyer: nevezes.vevo,
      items: tetelek(nevezes),
    },
    kliens,
  )
}

export async function dijbekeroTorlese(
  nevezes: Nevezes,
  kliens: Kassza = alapKliens,
): Promise<TorlesEredmenye> {
  const orderNumber = rendelesszam(nevezes)
  const legutobbi = await kliens.invoices.find({ orderNumber })
  if (legutobbi?.header.type === 'invoice') return 'mar-szamlazva'

  try {
    await kliens.invoices.deleteProforma({ orderNumber })
    return 'torolve'
  } catch (error) {
    if (isSzamlazzError(error) && error.isNotFound) return 'nem-talalhato'
    throw error
  }
}

A számla ugyanazt a vevőt és ugyanazokat a tételeket kapja, mint a díjbekérő. A proformaNumber kapcsolja össze a két bizonylatot a Számlázz.hu-ban.

Díjbekérő, majd számla

Díjbekérő kiállítása, és a befizetés után a hozzá kapcsolt végleges számla.

Futtatás a sandboxban
dijbekero-szamla.ts
import { , type InvoiceBuyer, type  } from 'kassza'

const  = ()

const : InvoiceBuyer = {
  : 'Futóklub Egyesület',
  : '1134',
  : 'Budapest',
  : 'Váci út 10.',
  : 'penztar@example.hu',
}

const : [] = [
  { : 'Nevezési díj, félmaraton', : 12_990, : 27 },
]

const  = await ..({
  : 'proforma',
  : 'NEV-2026-042',
  : ,
  : ,
})
.('Díjbekérő:', ., .)

const  = await ..({
  : .,
  : 'NEV-2026-042',
  : 'átutalás',
  : true,
  : ,
  : ,
})
.('Számla:', ., 'hátralék:', .)

Jelentkezés#

A @/lib/nevezesek a saját adatbázis-réteged: a nevezesMentese() elmenti a jelentkezést és visszaadja Nevezes alakban, a dijbekeroszamMentese() a nevezéshez írja a díjbekérő számát.

app/api/nevezesek/route.ts
import { isSzamlazzError } from 'kassza'
import { isValidEmail, isValidHungarianZipCode } from 'kassza/validators'
import { dijbekeroszamMentese, nevezesMentese } from '@/lib/nevezesek'
import { dijbekeroKiallitasa } from '@/lib/nevezes-szamlazas'

const TAVOK = {
  felmaraton: { nev: 'félmaraton', dij: 12_990 },
  maraton: { nev: 'maraton', dij: 16_990 },
} as const

type Tav = keyof typeof TAVOK

interface NevezesiUrlap {
  readonly nev?: unknown
  readonly email?: unknown
  readonly iranyitoszam?: unknown
  readonly varos?: unknown
  readonly cim?: unknown
  readonly tav?: unknown
}

function szoveg(ertek: unknown): string {
  return typeof ertek === 'string' ? ertek.trim() : ''
}

function ismertTav(ertek: unknown): ertek is Tav {
  return typeof ertek === 'string' && Object.hasOwn(TAVOK, ertek)
}

export async function POST(request: Request) {
  const urlap = (await request.json()) as NevezesiUrlap
  const nev = szoveg(urlap.nev)
  const email = szoveg(urlap.email)
  const iranyitoszam = szoveg(urlap.iranyitoszam)
  const varos = szoveg(urlap.varos)
  const cim = szoveg(urlap.cim)

  if (!nev || !varos || !cim || !isValidEmail(email) || !isValidHungarianZipCode(iranyitoszam)) {
    return Response.json({ hiba: 'Hiányos vagy hibás számlázási adatok.' }, { status: 400 })
  }
  if (!ismertTav(urlap.tav)) {
    return Response.json({ hiba: 'Ismeretlen táv.' }, { status: 400 })
  }

  const tav = TAVOK[urlap.tav]
  const nevezes = await nevezesMentese({
    vevo: { name: nev, email, zip: iranyitoszam, city: varos, address: cim },
    tav: tav.nev,
    nevezesiDij: tav.dij,
  })

  try {
    const dijbekeroszam = await dijbekeroKiallitasa(nevezes)
    await dijbekeroszamMentese(nevezes.id, dijbekeroszam)
    return Response.json({ id: nevezes.id, dijbekeroszam }, { status: 201 })
  } catch (error) {
    if (!isSzamlazzError(error)) throw error
    console.error('A díjbekérő nem készült el', nevezes.id, error.category, error.code)
    return Response.json({ id: nevezes.id, dijbekeroszam: null }, { status: 201 })
  }
}

Ha a díjbekérő nem készül el, a nevezés ettől még mentve van. Ha később újra meghívod a dijbekeroKiallitasa() függvényt, az a rendelésszám alapján megtalálja a mégis elkészült díjbekérőt, így a jelentkező nem kap kettőt.

Befizetés és lemondás#

A két végpontot a szervezők admin felülete hívja. Az adminKeres() a belső token ellenőrzése, a nevezesBetoltese(), a szamlaszamMentese() és a nevezesLemondasa() a saját adatbázis-függvényeid.

lib/admin.ts
export function adminKeres(request: Request): boolean {
  const token = process.env.BELSO_API_TOKEN
  return Boolean(token) && request.headers.get('authorization') === `Bearer ${token}`
}
app/api/nevezesek/[id]/befizetes/route.ts
import { isSzamlazzError } from 'kassza'
import { adminKeres } from '@/lib/admin'
import { szamlaBefizetesUtan } from '@/lib/nevezes-szamlazas'
import { nevezesBetoltese, szamlaszamMentese } from '@/lib/nevezesek'

export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
  if (!adminKeres(request)) return new Response('Nincs jogosultság', { status: 401 })

  const { id } = await params
  const nevezes = await nevezesBetoltese(id)
  if (!nevezes) return Response.json({ hiba: 'Nincs ilyen nevezés.' }, { status: 404 })

  try {
    const szamlaszam = await szamlaBefizetesUtan(nevezes)
    await szamlaszamMentese(nevezes.id, szamlaszam)
    return Response.json({ szamlaszam })
  } catch (error) {
    if (!isSzamlazzError(error)) throw error
    return Response.json({ hiba: error.message, tipp: error.hint }, { status: 502 })
  }
}
app/api/nevezesek/[id]/route.ts
import { adminKeres } from '@/lib/admin'
import { dijbekeroTorlese } from '@/lib/nevezes-szamlazas'
import { nevezesBetoltese, nevezesLemondasa } from '@/lib/nevezesek'

export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
  if (!adminKeres(request)) return new Response('Nincs jogosultság', { status: 401 })

  const { id } = await params
  const nevezes = await nevezesBetoltese(id)
  if (!nevezes) return Response.json({ hiba: 'Nincs ilyen nevezés.' }, { status: 404 })

  const eredmeny = await dijbekeroTorlese(nevezes)
  if (eredmeny === 'mar-szamlazva') {
    return Response.json(
      { hiba: 'A nevezési díj már be van fizetve, a számlát sztornózni kell.' },
      { status: 409 },
    )
  }

  await nevezesLemondasa(nevezes.id)
  return Response.json({ eredmeny })
}

Díjbekérő törlése

Díjbekérő törlése rendelésszám alapján, és a második törlés hibája.

Futtatás a sandboxban
dijbekero-torlese.ts
import { ,  } from 'kassza'

const  = ()

const  = await ..({
  : 'proforma',
  : 'NEV-2026-077',
  : { : 'Vevő Kft.', : '1111', : 'Budapest', : 'Fő utca 1.' },
  : [{ : 'Tábori részvételi díj', : 64_000, : 27 }],
})

await ..({ : 'NEV-2026-077' })
.('Törölt díjbekérő:', .)

try {
  await ..(.)
} catch () {
  if (!()) throw 
  .({
    : .,
    : .,
    : .,
    : .,
  })
}

Buktatók#

Kapcsolódó#

Oldal szerkesztéseUtoljára frissítve: