ENbext.dev
DocumentationFonctionnalités

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

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.

Conseil

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 :

  1. Envoyez le client vers une caisse hébergée pour un sac d’éléments de campagne.
  2. Vérifiez si un abonnement connu est toujours actif.
  3. 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#

rust
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#

rust
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#

rust
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#

rust
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.
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.