Stripe Integration
Process payments securely with Stripe integration.
Overview
Olympus Cloud uses Stripe for comprehensive payment processing:
| Feature | Description | Stripe API |
|---|---|---|
| Card Payments | Accept credit/debit cards | Payment Intents |
| Digital Wallets | Apple Pay, Google Pay | Payment Methods |
| Subscriptions | Recurring billing | Subscriptions API |
| Payouts | Send funds to vendors | Connect Payouts |
| Reporting | Financial reconciliation | Reporting API |
Setup
Prerequisites
- Stripe account with verified business
- API keys from Stripe Dashboard
- 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
| Event | Action |
|---|---|
payment_intent.succeeded | Mark order as paid |
payment_intent.payment_failed | Notify failure, retry |
charge.refunded | Process refund |
customer.subscription.created | Activate subscription |
customer.subscription.updated | Update plan |
customer.subscription.deleted | Cancel subscription |
invoice.payment_succeeded | Record payment |
invoice.payment_failed | Notify, 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
| Plan | Price ID | Interval | Features |
|---|---|---|---|
| Starter | price_starter_monthly | Monthly | 1 location, basic POS |
| Pro | price_pro_monthly | Monthly | 5 locations, KDS, API |
| Enterprise | price_enterprise_monthly | Monthly | Unlimited, 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
| Requirement | Implementation |
|---|---|
| No card data storage | Use Stripe Elements |
| Secure transmission | TLS 1.2+ only |
| Token-based payments | Payment Method tokens |
| Webhook verification | Signature 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 Number | Scenario |
|---|---|
| 4242424242424242 | Success |
| 4000000000000002 | Decline |
| 4000002500003155 | 3D Secure required |
| 4000000000009995 | Insufficient 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()),
}
}
Related Documentation
- Payments API - Payment endpoints
- Webhooks - Webhook configuration
- Orders API - Order management