Fournisseurs de paiement
La fonctionnalité de paiement enveloppe tout fournisseur de paiement hébergé (Stripe, Lemon Squeezy, Paddle,...) derrière un petit trait délibérément afin qu'une vitrine puisse créer des paiements, rechercher des abonnements et décoder des webhooks sans coder en dur le SDK d'un fournisseur.
La vérification de la signature Webhook est gérée par la fonctionnalité Webhook, et non par cette fonctionnalité. Exécutez toujours un vérificateur WebhookPlugin avant d’appeler decode_webhook_event.
Philosophie : un petit trait volontaire#
Stripe, Lemon Squeezy et Paddle ne sont pas d'accord sur la forme de leurs éléments de campagne, de leurs objets produits et des enveloppes des webhooks. Ils s'accordent sur trois choses dont chaque vitrine a besoin :
- Envoyez le client vers une caisse hébergée pour un sac d’éléments de campagne.
- Vérifiez si un abonnement connu est toujours actif.
- Décidez de la signification abstraite d’un événement webhook.
Le trait expose exactement ces trois opérations. Tout ce qui est spécifique au fournisseur (automatic_tax de Stripe, Lemon Squeezy's
variant_id, custom_data de Paddle ) vit dans un
Trappe de secours attributes: HashMap<String, String> sur chaque type de données. Le trait lui-même ne fait jamais apparaître un champ mentionnant un fournisseur spécifique.
Le trait#
pub trait PaymentPlugin: Send + Sync {
fn name(&self) -> &str;
fn provider(&self) -> &str; // "stripe", "lemonsqueezy", "paddle"
fn create_checkout(
&self,
items: Vec<LineItem>,
opts: CheckoutOpts,
) -> Result<CheckoutSession, PaymentError>;
fn get_subscription(&self, id: &str) -> Result<Subscription, PaymentError>;
fn decode_webhook_event(
&self,
raw_body: &[u8],
) -> Result<PaymentEvent, PaymentError>;
}
La vérification de la signature des webhooks est gérée séparément par un
WebhookPlugin ; decode_webhook_event suppose que le corps a déjà été authentifié.
Types de clés#
| Taper | But |
|---|---|
Money |
amount_cents: i64 + Currency. Cents entiers dans toutes les devises, y compris les devises sans décimale comme le JPY. |
Currency |
Code à trois lettres ISO-4217, validé à la construction. |
LineItem |
name, description, quantity, unit_price, sku. |
CheckoutOpts |
customer_email, success_url, cancel_url, metadata, mode. |
CheckoutMode |
OneTime ou Subscription { interval } avec Day / Week / Month / Year. |
CheckoutSession |
id, url, expires_at, attributes. |
Subscription |
id, customer_id, status, période de facturation, cancel_at_period_end, attributes. |
SubscriptionStatus |
Active, Trialing, PastDue, Canceled, Unpaid, Incomplete. |
PaymentEvent |
7 variantes : CheckoutCompleted, SubscriptionCreated, SubscriptionUpdated, SubscriptionCanceled, PaymentSucceeded, PaymentFailed, Unknown { event_type }. |
PaymentError |
InvalidCurrency (400), InvalidAmount (400), NotFound (404), ProviderError (502), Backend (500). |
Implémentations de référence#
| Plugin | Fournisseur | Remarques |
|---|---|---|
@bext/pay-stripe (en direct) |
Bande | StripeLivePaymentPlugin — appels REST réels api.stripe.com (sessions de paiement, recherche d'abonnement). |
@bext/pay-stripe (double) |
Bande | StripePaymentPlugin — double référence sans réseau ; sessions stub déterministes + le décodeur webhook partagé. |
@bext/pay-lemonsqueezy |
Pressé au Citron | Double référence sans réseau (le client live Lemon Squeezy est un travail futur). |
Le serveur sélectionne le plugin Stripe par configuration : set
BEXT_PAYMENT_STRIPE_KEY (ou BEXT_PAYMENT_API_KEY ) vers une clé secrète et le client live est utilisé (véritable HTTP vers Stripe) ; sans jeu de clés, le double** sans réseau **est utilisé afin que les tests et les environnements hors ligne exercent le contrat de trait sans avoir recours à une API en direct. Le Webhook décodant est partagé par les deux et est toujours disponible ; La vérification de la signature du webhook est une préoccupation distincte traitée par @bext/webhook-verifiers (véritable HMAC-SHA256) devant le gestionnaire de paiement.
Exemple : créer une caisse#
use bext_plugin_api::payment::*;
use std::collections::HashMap;
let plugin: &dyn PaymentPlugin = /* @bext/pay-stripe */;
let session = plugin.create_checkout(
vec![LineItem {
name: "Pro Plan".into(),
description: Some("Monthly access".into()),
quantity: 1,
unit_price: Money::new(1999, Currency::from_code("USD")?),
sku: Some("PRO-M-1".into()),
}],
CheckoutOpts {
customer_email: Some("alice@example.com".into()),
success_url: "https://app.example.com/thanks".into(),
cancel_url: "https://app.example.com/pricing".into(),
metadata: HashMap::new(),
mode: CheckoutMode::Subscription {
interval: SubscriptionInterval::Month,
},
},
)?;
// Redirect the customer
redirect(&session.url);
Exemple : gérer un webhook#
fn on_webhook(plugin: &dyn PaymentPlugin, raw: &[u8]) -> Result<(), PaymentError> {
// Signature is verified by a WebhookPlugin upstream of this call.
match plugin.decode_webhook_event(raw)? {
PaymentEvent::CheckoutCompleted { session_id, customer_id } => {
mark_paid(&session_id, customer_id.as_deref());
}
PaymentEvent::SubscriptionCreated(sub)
| PaymentEvent::SubscriptionUpdated(sub) => {
upsert_subscription(&sub);
}
PaymentEvent::SubscriptionCanceled { subscription_id } => {
revoke_access(&subscription_id);
}
PaymentEvent::PaymentSucceeded { amount, customer_id } => {
record_payment(amount, customer_id.as_deref());
}
PaymentEvent::PaymentFailed { reason, customer_id } => {
alert_dunning(&reason, customer_id.as_deref());
}
PaymentEvent::Unknown { event_type } => {
tracing::info!(event_type, "unknown payment event (ignored)");
}
}
Ok(())
}
Exemple : rechercher un abonnement#
let sub = plugin.get_subscription("sub_abc123")?;
if sub.status == SubscriptionStatus::Active && !sub.cancel_at_period_end {
allow_pro_features(&sub.customer_id);
}
Règles de change#
Currency::from_code rejette tout ce qui ne contient pas exactement trois lettres ASCII majuscules. La liste ISO elle-même n'est pas appliquée lors de la construction (c'est le travail du processeur de paiement à la limite de l'API), mais les fautes de frappe comme "usd" ou "US" échouent rapidement.
Money est signé ( i64 ), donc les remboursements et les ajustements aller-retour via le même type. Les devises sans décimale (JPY, KRW) utilisent le même champ ; le plugin divise par 1 au lieu de 100.
Indicateur de fonctionnalité#
Aucun. Le trait et les types de paiement résident dans bext-plugin-api et sont toujours disponibles ; aucune fonction de chargement ne les bloque.
Voir aussi#
@bext/pay-stripe— Implémentation de référence Stripe.@bext/pay-lemonsqueezy— Implémentation de référence Lemon Squeezy.- Webhook — gère la vérification de la signature avant l'exécution de
decode_webhook_event. - Aperçu des capacités — la liste complète des fonctionnalités enfichables.
- Session — où l'identité du client passe par les métadonnées de paiement.