Skip to main content

Stripe Integration

Process payments securely with Stripe integration.

Overview

Olympus Cloud uses Stripe for comprehensive payment processing:

FeatureDescriptionStripe API
Card PaymentsAccept credit/debit cardsPayment Intents
Digital WalletsApple Pay, Google PayPayment Methods
SubscriptionsRecurring billingSubscriptions API
PayoutsSend funds to vendorsConnect Payouts
ReportingFinancial reconciliationReporting API

Setup

Prerequisites

  1. Stripe account with verified business
  2. API keys from Stripe Dashboard
  3. Webhook endpoint configured

Configuration

# config/stripe.yaml
stripe:
# API keys (use Secret Manager in production)
publishable_key: pk_test_xxx
secret_key: sk_test_xxx
webhook_secret: whsec_xxx

# Connect settings (for marketplace)
connect:
enabled: true
application_fee_percent: 2.5

# Payment settings
payments:
capture_method: automatic # or manual
currency: usd
statement_descriptor: "OLYMPUS CLOUD"

Environment Variables

# Production (use Secret Manager)
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_PUBLISHABLE_KEY=pk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

Payment Processing

Create Payment Intent

// src/payments/stripe.rs
use stripe::{
Client, PaymentIntent, CreatePaymentIntent, Currency,
PaymentIntentCaptureMethod,
};

pub struct StripeService {
client: Client,
}

impl StripeService {
pub async fn create_payment_intent(
&self,
amount: i64,
currency: Currency,
metadata: HashMap<String, String>,
) -> Result<PaymentIntent> {
let mut params = CreatePaymentIntent::new(amount, currency);
params.capture_method = Some(PaymentIntentCaptureMethod::Automatic);
params.metadata = Some(metadata);
params.statement_descriptor = Some("OLYMPUS CLOUD".into());

PaymentIntent::create(&self.client, params).await
}

pub async fn confirm_payment(
&self,
payment_intent_id: &str,
payment_method_id: &str,
) -> Result<PaymentIntent> {
let mut params = stripe::UpdatePaymentIntent::default();
params.payment_method = Some(payment_method_id.into());

PaymentIntent::update(&self.client, payment_intent_id, params).await?;

PaymentIntent::confirm(&self.client, payment_intent_id, None).await
}
}

Client-Side Integration

// Frontend: Create payment
import { loadStripe } from '@stripe/stripe-js';

const stripe = await loadStripe('pk_live_xxx');

async function processPayment(orderId: string) {
// Get payment intent from backend
const response = await fetch('/api/v1/orders/' + orderId + '/payment-intent', {
method: 'POST',
});
const { clientSecret } = await response.json();

// Confirm payment
const { error, paymentIntent } = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: cardElement,
billing_details: {
name: 'Customer Name',
},
},
});

if (error) {
console.error('Payment failed:', error.message);
} else if (paymentIntent.status === 'succeeded') {
console.log('Payment successful!');
}
}

Webhook Handling

Webhook Endpoint

// src/webhooks/stripe.rs
use stripe::{Event, EventType, Webhook};

pub async fn handle_stripe_webhook(
payload: &str,
signature: &str,
webhook_secret: &str,
) -> Result<()> {
let event = Webhook::construct_event(payload, signature, webhook_secret)?;

match event.type_ {
EventType::PaymentIntentSucceeded => {
let payment_intent = event.data.object.as_payment_intent()?;
handle_payment_succeeded(payment_intent).await?;
}
EventType::PaymentIntentPaymentFailed => {
let payment_intent = event.data.object.as_payment_intent()?;
handle_payment_failed(payment_intent).await?;
}
EventType::ChargeRefunded => {
let charge = event.data.object.as_charge()?;
handle_refund(charge).await?;
}
EventType::CustomerSubscriptionCreated |
EventType::CustomerSubscriptionUpdated |
EventType::CustomerSubscriptionDeleted => {
let subscription = event.data.object.as_subscription()?;
handle_subscription_change(subscription).await?;
}
_ => {
tracing::info!("Unhandled webhook event: {:?}", event.type_);
}
}

Ok(())
}

async fn handle_payment_succeeded(payment_intent: &PaymentIntent) -> Result<()> {
let order_id = payment_intent.metadata.get("order_id")
.ok_or(Error::MissingMetadata)?;

// Update order status
order_service.mark_paid(order_id, PaymentInfo {
provider: "stripe",
transaction_id: payment_intent.id.to_string(),
amount: payment_intent.amount,
currency: payment_intent.currency.to_string(),
}).await?;

// Send confirmation
notification_service.send_payment_confirmation(order_id).await?;

Ok(())
}

Webhook Events

EventAction
payment_intent.succeededMark order as paid
payment_intent.payment_failedNotify failure, retry
charge.refundedProcess refund
customer.subscription.createdActivate subscription
customer.subscription.updatedUpdate plan
customer.subscription.deletedCancel subscription
invoice.payment_succeededRecord payment
invoice.payment_failedNotify, retry

Stripe Connect (Marketplace)

Onboarding Restaurants

// src/payments/connect.rs
use stripe::{Account, AccountLink, CreateAccount, CreateAccountLink};

pub async fn create_connected_account(
tenant_id: &str,
business_profile: BusinessProfile,
) -> Result<Account> {
let mut params = CreateAccount::new();
params.type_ = Some(stripe::AccountType::Express);
params.country = Some("US");
params.email = Some(business_profile.email.as_str());
params.business_profile = Some(stripe::CreateAccountBusinessProfile {
name: Some(business_profile.name.clone()),
mcc: Some("5812"), // Restaurants
..Default::default()
});
params.metadata = Some(hashmap! {
"tenant_id" => tenant_id.to_string(),
});

Account::create(&self.client, params).await
}

pub async fn create_onboarding_link(
account_id: &str,
return_url: &str,
) -> Result<String> {
let params = CreateAccountLink {
account: account_id.into(),
type_: stripe::AccountLinkType::AccountOnboarding,
refresh_url: Some(format!("{}/stripe/refresh", return_url)),
return_url: Some(format!("{}/stripe/complete", return_url)),
..Default::default()
};

let link = AccountLink::create(&self.client, params).await?;
Ok(link.url)
}

Split Payments

// Process payment with platform fee
pub async fn create_connected_payment(
&self,
amount: i64,
connected_account_id: &str,
application_fee: i64,
) -> Result<PaymentIntent> {
let mut params = CreatePaymentIntent::new(amount, Currency::USD);
params.application_fee_amount = Some(application_fee);
params.transfer_data = Some(stripe::CreatePaymentIntentTransferData {
destination: connected_account_id.into(),
..Default::default()
});

PaymentIntent::create(&self.client, params).await
}

Subscriptions

Create Subscription

// src/billing/subscriptions.rs
use stripe::{Subscription, CreateSubscription, CreateSubscriptionItems};

pub async fn create_subscription(
customer_id: &str,
price_id: &str,
trial_days: Option<u32>,
) -> Result<Subscription> {
let mut params = CreateSubscription::new(customer_id);
params.items = Some(vec![CreateSubscriptionItems {
price: Some(price_id.into()),
quantity: Some(1),
..Default::default()
}]);

if let Some(days) = trial_days {
params.trial_period_days = Some(days);
}

params.payment_behavior = Some(
stripe::SubscriptionPaymentBehavior::DefaultIncomplete
);

Subscription::create(&self.client, params).await
}

Subscription Plans

PlanPrice IDIntervalFeatures
Starterprice_starter_monthlyMonthly1 location, basic POS
Proprice_pro_monthlyMonthly5 locations, KDS, API
Enterpriseprice_enterprise_monthlyMonthlyUnlimited, AI features

Refunds

Process Refund

// src/payments/refunds.rs
use stripe::{Refund, CreateRefund};

pub async fn create_refund(
payment_intent_id: &str,
amount: Option<i64>,
reason: RefundReason,
) -> Result<Refund> {
let mut params = CreateRefund::default();
params.payment_intent = Some(payment_intent_id.into());
params.amount = amount;
params.reason = Some(match reason {
RefundReason::CustomerRequest => stripe::RefundReason::RequestedByCustomer,
RefundReason::Duplicate => stripe::RefundReason::Duplicate,
RefundReason::Fraudulent => stripe::RefundReason::Fraudulent,
});

Refund::create(&self.client, params).await
}

PCI Compliance

Requirements

RequirementImplementation
No card data storageUse Stripe Elements
Secure transmissionTLS 1.2+ only
Token-based paymentsPayment Method tokens
Webhook verificationSignature validation

Best Practices

// Never log card details
// DO NOT DO THIS:
// console.log(cardNumber);

// Use Stripe Elements for secure card input
const elements = stripe.elements();
const cardElement = elements.create('card', {
style: {
base: {
fontSize: '16px',
color: '#424770',
},
},
hidePostalCode: true,
});

cardElement.mount('#card-element');

Testing

Test Cards

Card NumberScenario
4242424242424242Success
4000000000000002Decline
40000025000031553D Secure required
4000000000009995Insufficient funds

Test Mode

# Use test API keys
STRIPE_SECRET_KEY=sk_test_xxx

# Forward test webhooks to the dev environment
stripe listen --forward-to https://dev.api.olympuscloud.ai/api/v1/webhooks/stripe

Error Handling

use stripe::StripeError;

pub fn handle_stripe_error(error: StripeError) -> ApiError {
match error {
StripeError::CardError { message, code, .. } => {
ApiError::PaymentFailed {
message,
code: code.map(|c| c.to_string()),
}
}
StripeError::InvalidRequestError { message, .. } => {
ApiError::BadRequest(message)
}
StripeError::AuthenticationError { .. } => {
ApiError::InternalError("Payment configuration error".into())
}
_ => ApiError::InternalError("Payment processing error".into()),
}
}