Jak přežít Stripe implementaci

Jak přežít Stripe implementaci

Tento článek se věnuje implementaci Stripe a vychází z doporučení, která představil Theo Browne. Názor, který zde najdete, není výhradně můj, ale odráží i jeho zkušenosti s řešením problémů, jež přicházejí při synchronizaci dat mezi Stripe a vaší aplikací.

Výchozí koncept

Theo Browne ve svém článku upozorňuje na tzv. "split brain" stav, kdy Stripe a vaše aplikace mají odlišný přehled o stavu platby. Tento problém vychází z rozdílného zpracování událostí a je potřeba ho řešit centralizovaným přístupem. Mým cílem je představit vám, jak využít doporučenou strategii a vyhnout se tak chybám, které mohou vzniknout při nesynchronizovaných aktualizacích.

Příprava systému

Podle Thea je nutné, aby byl váš projekt postaven na pevných základech. To znamená mít funkční backend postavený na JavaScriptu s využitím TypeScriptu a ověřenou autentizací. Velmi důležitou součástí je také KV úložiště, například Redis nebo Upstash, které slouží jako centrální místo pro ukládání aktuálních informací o stavu platby.

Proces vytvoření zákazníka a Checkout session

Klíčovým bodem, na který Theo opakovaně poukazuje, je, že uživatel by měl mít před zahájením platby již přiřazeného zákazníka ve Stripe. Pokud toto není splněno, může dojít k problémům, například při vícenásobném spuštění platebního procesu. Proto je potřeba nejprve ověřit existenci vazby mezi uživatelským ID a Stripe customer ID. V případě, že tato vazba neexistuje, vytvoří se nový zákazník a příslušné ID se uloží do KV úložiště. Následně se vytvoří Checkout session, kdy je zákazník explicitně předán jako parametr, což výrazně snižuje riziko chyb.

import stripe from 'stripe';

const stripeClient = stripe(process.env.STRIPE_SECRET_KEY);

export async function createCheckoutSession(user) {
  let stripeCustomerId = await kv.get(`stripe:user:${user.id}`);
  if (!stripeCustomerId) {
    const newCustomer = await stripeClient.customers.create({
      email: user.email,
      metadata: { userId: user.id }
    });
    await kv.set(`stripe:user:${user.id}`, newCustomer.id);
    stripeCustomerId = newCustomer.id;
  }

  const session = await stripeClient.checkout.sessions.create({
    customer: stripeCustomerId,
    payment_method_types: ['card'],
    line_items: [{
      price: process.env.STRIPE_PRICE_ID,
      quantity: 1
    }],
    mode: 'subscription',
    success_url: 'https://vasestranka.cz/success',
    cancel_url: 'https://vasestranka.cz/cancel'
  });
  return session;
}

Centralizovaná synchronizace dat

Důležitou součástí implementace, jak ji popisuje Theo, je centralizovaná synchronizační funkce. Tato funkce, označovaná jako **syncStripeDataToKV**, je zodpovědná za načtení aktuálních údajů o předplatném či platbě přímo z Stripe a jejich následné uložení do KV úložiště. Díky tomu může vaše aplikace pracovat s konzistentním stavem platby, což je klíčové zejména při asynchronním doručování událostí. I když je implementace tohoto přístupu náročnější, přináší výrazně vyšší stabilitu celého systému.

Mít jednu funkci, která synchronizuje data ze Stripe do KV, může být záchranou v chaotickém světě platebních systémů.

export async function syncStripeDataToKV(customerId) {
  try {
    const subscriptions = await stripeClient.subscriptions.list({
      customer: customerId,
      limit: 1,
      status: 'all',
      expand: ['data.default_payment_method']
    });

    if (subscriptions.data.length === 0) {
      const subData = { status: 'none' };
      await kv.set(`stripe:customer:${customerId}`, subData);
      return subData;
    }

    const subscription = subscriptions.data[0];
    const subData = {
      subscriptionId: subscription.id,
      status: subscription.status,
      priceId: subscription.items.data[0].price.id,
      currentPeriodStart: subscription.current_period_start,
      currentPeriodEnd: subscription.current_period_end,
      cancelAtPeriodEnd: subscription.cancel_at_period_end,
      paymentMethod: subscription.default_payment_method && typeof subscription.default_payment_method !== 'string'
        ? {
            brand: subscription.default_payment_method.card?.brand || null,
            last4: subscription.default_payment_method.card?.last4 || null
          }
        : null
    };

    await kv.set(`stripe:customer:${customerId}`, subData);
    return subData;
  } catch (error) {
    console.error('Chyba při synchronizaci dat se Stripe:', error);
    throw error;
  }
}

Role webhooků

Theo zdůrazňuje, že nastavení webhooků je nezbytné pro okamžitou aktualizaci stavu platby. Webhook naslouchá událostem, jako jsou dokončení Checkout session nebo změny stavu předplatného, a automaticky spouští synchronizační funkci. Tento přístup odstraňuje potřebu zpracovávat informace z více zdrojů, což značně snižuje riziko nekonzistence dat. Je to další krok, který zajišťuje, že vaše aplikace vždy pracuje s aktuálními a spolehlivými informacemi.

import { NextResponse } from 'next/server';

export async function POST(req) {
  const body = await req.text();
  const signature = req.headers.get('Stripe-Signature');

  if (!signature) return NextResponse.json({}, { status: 400 });

  try {
    const event = stripeClient.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    );

    if ([
      'checkout.session.completed',
      'customer.subscription.created',
      'customer.subscription.updated',
      'customer.subscription.deleted'
    ].includes(event.type)) {
      const { customer: customerId } = event.data.object;
      if (typeof customerId === 'string') {
        await syncStripeDataToKV(customerId);
      }
    }
    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Chyba při zpracování Stripe webhooku:', error);
    return NextResponse.json({ error: 'Chyba při zpracování' }, { status: 400 });
  }
}

export const config = {
  api: { bodyParser: false }
};

SDÍLET: