Stripe-integraatio
Kirjapro käyttää Stripea tilauslaskutukseen. Tämä dokumentaatio kuvaa integraation toiminnan kehittäjille.
Yleiskatsaus
Osio nimeltä “Yleiskatsaus”┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐│ Kirjapro │────▶│ Supabase Edge │────▶│ Stripe API ││ Frontend │ │ Functions │ │ │└─────────────────┘ └──────────────────┘ └─────────────────┘ │ │ ▼ ▼ ┌──────────────────┐ ┌─────────────────┐ │ Tietokanta │◀────│ Webhooks │ │ │ │ (tilaukset) │ └──────────────────┘ └─────────────────┘Tilaustasot
Osio nimeltä “Tilaustasot”| Taso | Kuukausi | Vuosi | Ominaisuudet |
|---|---|---|---|
| Ilmainen | 0 € | 0 € | Perustoiminnot |
| Yrittäjä | 19 € | 190 € | Rajaton laskutus, pankkiyhteys |
| Yritys | 49 € | 490 € | Kaikki ominaisuudet, 5 käyttäjää |
Checkout-flow
Osio nimeltä “Checkout-flow”Tilauksen aloitus
Osio nimeltä “Tilauksen aloitus”- Käyttäjä valitsee hinnoittelusivulla haluamansa paketin
- Frontend kutsuu
create-checkout-sessionendpointia - Edge function luo Stripe Checkout -session
- Käyttäjä ohjataan Stripen maksusivulle
- Maksun jälkeen webhook päivittää tilauksen
API-kutsu
Osio nimeltä “API-kutsu”const response = await fetch( `${SUPABASE_URL}/functions/v1/create-checkout-session`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'apikey': SUPABASE_ANON_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ priceId: 'price_xxx', companyId: 'uuid-here' }), });
const { checkoutUrl } = await response.json();window.location.href = checkoutUrl;Vastaus
Osio nimeltä “Vastaus”{ "success": true, "checkoutUrl": "https://checkout.stripe.com/c/pay/...", "sessionId": "cs_xxx"}Customer Portal
Osio nimeltä “Customer Portal”Tilauksen hallinta tapahtuu Stripen asiakasportaalissa.
Portaalin avaaminen
Osio nimeltä “Portaalin avaaminen”const response = await fetch( `${SUPABASE_URL}/functions/v1/create-portal-session`, { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'apikey': SUPABASE_ANON_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ companyId: 'uuid-here', returnUrl: 'https://app.kirjapro.fi/settings' }), });
const { portalUrl } = await response.json();window.location.href = portalUrl;Portaalin toiminnot
Osio nimeltä “Portaalin toiminnot”Asiakasportaalissa voi:
- Päivittää maksutapaa
- Vaihtaa tilaustasoa
- Nähdä laskuhistorian
- Peruuttaa tilauksen
Webhook-tapahtumat
Osio nimeltä “Webhook-tapahtumat”Stripe lähettää webhook-kutsuja tilauksen tilan muuttuessa.
Tuetut tapahtumat
Osio nimeltä “Tuetut tapahtumat”| Tapahtuma | Kuvaus |
|---|---|
checkout.session.completed | Uusi tilaus aloitettu |
customer.subscription.updated | Tilaus päivitetty (taso, jakso) |
customer.subscription.deleted | Tilaus peruttu |
invoice.paid | Lasku maksettu |
invoice.payment_failed | Maksu epäonnistui |
Webhook-payload
Osio nimeltä “Webhook-payload”{ "type": "customer.subscription.updated", "data": { "object": { "id": "sub_xxx", "customer": "cus_xxx", "status": "active", "items": { "data": [{ "price": { "id": "price_xxx", "unit_amount": 1900, "recurring": { "interval": "month" } } }] }, "current_period_start": 1704067200, "current_period_end": 1706745600 } }}Allekirjoituksen varmistus
Osio nimeltä “Allekirjoituksen varmistus”import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY);const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent( rawBody, sig, STRIPE_WEBHOOK_SECRET);
// Käsittele tapahtumaswitch (event.type) { case 'checkout.session.completed': // Uusi tilaus break; case 'customer.subscription.updated': // Tilaus päivitetty break;}import stripe
stripe.api_key = STRIPE_SECRET_KEY
payload = request.bodysig = request.headers['Stripe-Signature']
event = stripe.Webhook.construct_event( payload, sig, STRIPE_WEBHOOK_SECRET)
if event['type'] == 'checkout.session.completed': # Uusi tilaus passelif event['type'] == 'customer.subscription.updated': # Tilaus päivitetty passTilauksen tila
Osio nimeltä “Tilauksen tila”Status-arvot
Osio nimeltä “Status-arvot”| Status | Merkitys |
|---|---|
trialing | Kokeilujakso käynnissä |
active | Aktiivinen tilaus |
past_due | Maksu myöhässä |
canceled | Peruttu |
Tilan tarkistus
Osio nimeltä “Tilan tarkistus”// Haetaan yrityksen tilausconst { data: subscription } = await supabase .from('subscriptions') .select('tier, status, current_period_end') .eq('company_id', companyId) .single();
if (subscription?.status === 'active') { // Tilaus voimassa}
if (subscription?.status === 'past_due') { // Näytä maksukehotus}Rate Limiting
Osio nimeltä “Rate Limiting”| Endpoint | Raja |
|---|---|
| create-checkout-session | 5 / min |
| create-portal-session | 5 / min |
| stripe-webhook | Ei rajaa |
Virhekäsittely
Osio nimeltä “Virhekäsittely”Yleiset virheet
Osio nimeltä “Yleiset virheet”| Koodi | Viesti | Ratkaisu |
|---|---|---|
STRIPE_CUSTOMER_NOT_FOUND | Ei Stripe-asiakasta | Käyttäjän tulee ensin tehdä tilaus |
INVALID_PRICE_ID | Virheellinen hinta | Tarkista price ID |
SUBSCRIPTION_NOT_FOUND | Tilausta ei löydy | Yritys ei ole tilaaja |
PORTAL_SESSION_FAILED | Portaali-istunto epäonnistui | Kokeile uudelleen |
Virheellinen vastaus
Osio nimeltä “Virheellinen vastaus”{ "success": false, "error": { "code": "STRIPE_CUSTOMER_NOT_FOUND", "message": "Yritystä ei löydy Stripe-asiakkaana" }}Testaus
Osio nimeltä “Testaus”Testikortit
Osio nimeltä “Testikortit”Käytä Stripen testikortteja kehitysympäristössä:
| Kortti | Tulos |
|---|---|
| 4242 4242 4242 4242 | Onnistuu |
| 4000 0000 0000 0002 | Hylätään |
| 4000 0025 0000 3155 | Vaatii 3D Secure |
Stripe CLI
Osio nimeltä “Stripe CLI”# Kuuntele webhookeja lokaalististripe listen --forward-to localhost:54321/functions/v1/stripe-webhook
# Lähetä testitapahtumastripe trigger checkout.session.completedKäyttöesimerkki
Osio nimeltä “Käyttöesimerkki”React-hook
Osio nimeltä “React-hook”import { useState } from 'react';import { supabase } from '@/lib/supabase';
export function useStripeCheckout() { const [isLoading, setIsLoading] = useState(false);
const checkout = async (priceId: string, companyId: string) => { setIsLoading(true); try { const { data, error } = await supabase.functions.invoke( 'create-checkout-session', { body: { priceId, companyId } } );
if (error) throw error; window.location.href = data.checkoutUrl; } catch (err) { console.error('Checkout failed:', err); throw err; } finally { setIsLoading(false); } };
const manageSubscription = async (companyId: string) => { setIsLoading(true); try { const { data, error } = await supabase.functions.invoke( 'create-portal-session', { body: { companyId, returnUrl: window.location.href } } );
if (error) throw error; window.location.href = data.portalUrl; } finally { setIsLoading(false); } };
return { checkout, manageSubscription, isLoading };}Käyttö komponentissa
Osio nimeltä “Käyttö komponentissa”function PricingCard({ priceId, companyId }) { const { checkout, isLoading } = useStripeCheckout();
return ( <button onClick={() => checkout(priceId, companyId)} disabled={isLoading} > {isLoading ? 'Ladataan...' : 'Tilaa'} </button> );}Turvallisuus
Osio nimeltä “Turvallisuus”- Kaikki API-kutsut vaativat autentikoinnin
- Webhook-allekirjoitukset varmistetaan
- Return URL:t validoidaan (estää open redirect)
- Hinnat validoidaan sallittujen joukosta