Ugrás a tartalomra
kassza

Stripe webhook

A vevő a Stripe Checkout oldalán fizet, a Stripe pedig checkout.session.completed eseményt küld a webhook végpontodra. A végpont ellenőrzi az aláírást, megkeresi a rendelést, és kiállítja a számlát. A Stripe addig küldi újra az eseményt, amíg 2xx választ nem kap, ezért a számlázásnak idempotensnek kell lennie.

A recept a Fizetett rendelés számlája recept szamlazFizetettRendelest() függvényét használja, amely kiállítás előtt rendelésszám alapján megkeresi a számlát.

Telepítés#

npm install stripe
.env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...

A Checkout munkamenet#

A webhook két dolgot vár a munkamenettől: a saját rendelésazonosítódat a metadata mezőben, és a vevő számlázási címét. A cím nélkül nem lehet számlát kiállítani. A stripeTetelek() a saját függvényed, amely a rendelés sorait a Stripe line_items formátumára alakítja.

app/api/checkout/route.ts
import Stripe from 'stripe'
import { rendelesBetoltese, stripeTetelek } from '@/lib/rendelesek'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

export async function POST(request: Request) {
  const { rendelesId } = (await request.json()) as { rendelesId: string }
  const rendeles = await rendelesBetoltese(rendelesId)
  if (!rendeles) return Response.json({ hiba: 'Nincs ilyen rendelés.' }, { status: 404 })

  const session = await stripe.checkout.sessions.create({
    mode: 'payment',
    line_items: stripeTetelek(rendeles),
    billing_address_collection: 'required',
    metadata: { rendelesId: rendeles.id },
    success_url: `${process.env.SITE_URL}/rendeles/${rendeles.id}/koszonjuk`,
    cancel_url: `${process.env.SITE_URL}/kosar`,
  })

  return Response.json({ url: session.url })
}

A webhook#

A @/lib/rendelesek a saját adatbázis-réteged. A szamlazasiKiserlet() megnöveli és visszaadja a rendelés számlázási kísérleteinek számát, a kezelesreVar() pedig emberi ellenőrzésre jelöli a rendelést.

app/api/stripe/webhook/route.ts
import { type InvoiceBuyer, isSzamlazzError } from 'kassza'
import Stripe from 'stripe'
import {
  kezelesreVar,
  rendelesBetoltese,
  szamlaszamMentese,
  szamlazasiKiserlet,
} from '@/lib/rendelesek'
import { szamlazFizetettRendelest } from '@/lib/szamlazas'

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)

const MAX_KISERLET = 3

const ORSZAGNEVEK = new Intl.DisplayNames(['hu'], { type: 'region' })

function vevoStripebol(session: Stripe.Checkout.Session): InvoiceBuyer | undefined {
  const adatok = session.customer_details
  const cim = adatok?.address
  if (!adatok?.name || !cim?.postal_code || !cim.city || !cim.line1) return undefined
  return {
    name: adatok.name,
    country: cim.country && cim.country !== 'HU' ? ORSZAGNEVEK.of(cim.country) : undefined,
    zip: cim.postal_code,
    city: cim.city,
    address: [cim.line1, cim.line2].filter(Boolean).join(', '),
    email: adatok.email ?? undefined,
  }
}

export async function POST(request: Request) {
  const signature = request.headers.get('stripe-signature')
  if (!signature) return new Response('Hiányzó aláírás', { status: 400 })

  const payload = await request.text()
  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(payload, signature, process.env.STRIPE_WEBHOOK_SECRET!)
  } catch {
    return new Response('Érvénytelen aláírás', { status: 400 })
  }

  if (
    event.type !== 'checkout.session.completed' &&
    event.type !== 'checkout.session.async_payment_succeeded'
  ) {
    return new Response(null, { status: 200 })
  }

  const session = event.data.object
  if (session.payment_status !== 'paid') return new Response(null, { status: 200 })

  const rendelesId = session.metadata?.rendelesId
  const rendeles = rendelesId ? await rendelesBetoltese(rendelesId) : null
  if (!rendeles) {
    console.error('Stripe fizetés ismeretlen rendeléshez', session.id)
    return new Response(null, { status: 200 })
  }
  if (rendeles.szamlaszam) return new Response(null, { status: 200 })

  const vevo = vevoStripebol(session)
  if (!vevo) {
    await kezelesreVar(rendeles.id, 'A Stripe munkamenetből hiányzik a számlázási cím.')
    return new Response(null, { status: 200 })
  }

  if ((await szamlazasiKiserlet(rendeles.id)) > MAX_KISERLET) {
    await kezelesreVar(rendeles.id, 'Túl sok sikertelen számlázási kísérlet.')
    return new Response(null, { status: 200 })
  }

  try {
    const szamlaszam = await szamlazFizetettRendelest({
      id: rendeles.id,
      fizetesiMod: 'bankkártya',
      vevo,
      tetelek: rendeles.tetelek,
    })
    await szamlaszamMentese(rendeles.id, szamlaszam)
    return new Response(null, { status: 200 })
  } catch (error) {
    if (!isSzamlazzError(error)) throw error
    if (error.retryable) return new Response('A Számlázz.hu most nem elérhető', { status: 503 })
    await kezelesreVar(rendeles.id, `${error.category}: ${error.message}`)
    return new Response(null, { status: 200 })
  }
}

A válaszkódok szándékosak:

HelyzetVálaszMi történik
Hibás vagy hiányzó aláírás400A Stripe jelzi a hibát a Dashboardon.
Más esemény, fizetetlen munkamenet, már számlázott rendelés200Nincs teendő.
Hálózati hiba, időtúllépés vagy karbantartás, és a számla nem készült el503A Stripe később újraküldi az eseményt, és a függvény újra lekérdez a kiállítás előtt.
Validációs, fiók- vagy egyéb végleges hiba200A rendelés emberi ellenőrzésre vár, az újraküldés nem segítene.
Adatbázis- vagy egyéb nem kassza hiba500A Stripe újraküldi az eseményt.

Buktatók#

Kapcsolódó#

Oldal szerkesztéseUtoljára frissítve: