# Stripe Payment Integration - Multi-Tenant Ecommerce Platform ## Architecture Overview The payment integration uses **Stripe Connect** to handle multi-store payments, enabling: - Each store to receive payments directly - Platform to collect fees/commissions - Proper financial isolation between stores - Compliance with financial regulations ## Payment Models ### Database Models ```python # models/database/payment.py from decimal import Decimal from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text, Numeric from sqlalchemy.orm import relationship from app.core.database import Base from .base import TimestampMixin class StorePaymentConfig(Base, TimestampMixin): """Store-specific payment configuration.""" __tablename__ = "store_payment_configs" id = Column(Integer, primary_key=True, index=True) store_id = Column(Integer, ForeignKey("stores.id"), nullable=False, unique=True) # Stripe Connect configuration stripe_account_id = Column(String(255)) # Stripe Connect account ID stripe_account_status = Column(String(50)) # pending, active, restricted, inactive stripe_onboarding_url = Column(Text) # Onboarding link for store stripe_dashboard_url = Column(Text) # Store's Stripe dashboard # Payment settings accepts_payments = Column(Boolean, default=False) currency = Column(String(3), default="EUR") platform_fee_percentage = Column(Numeric(5, 2), default=2.5) # Platform commission # Payout settings payout_schedule = Column(String(20), default="weekly") # daily, weekly, monthly minimum_payout = Column(Numeric(10, 2), default=20.00) # Relationships store = relationship("Store", back_populates="payment_config") def __repr__(self): return f"" class Payment(Base, TimestampMixin): """Payment records for orders.""" __tablename__ = "payments" id = Column(Integer, primary_key=True, index=True) store_id = Column(Integer, ForeignKey("stores.id"), nullable=False) order_id = Column(Integer, ForeignKey("orders.id"), nullable=False) customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False) # Stripe payment details stripe_payment_intent_id = Column(String(255), unique=True, index=True) stripe_charge_id = Column(String(255), index=True) stripe_transfer_id = Column(String(255)) # Transfer to store account # Payment amounts (in cents to avoid floating point issues) amount_total = Column(Integer, nullable=False) # Total customer payment amount_store = Column(Integer, nullable=False) # Amount to store amount_platform_fee = Column(Integer, nullable=False) # Platform commission currency = Column(String(3), default="EUR") # Payment status status = Column(String(50), nullable=False) # pending, succeeded, failed, refunded payment_method = Column(String(50)) # card, bank_transfer, etc. # Metadata stripe_metadata = Column(Text) # JSON string of Stripe metadata failure_reason = Column(Text) refund_reason = Column(Text) # Timestamps paid_at = Column(DateTime) refunded_at = Column(DateTime) # Relationships store = relationship("Store") order = relationship("Order", back_populates="payment") customer = relationship("Customer") def __repr__(self): return f"" @property def amount_total_euros(self): """Convert cents to euros for display.""" return self.amount_total / 100 @property def amount_store_euros(self): """Convert cents to euros for display.""" return self.amount_store / 100 class PaymentMethod(Base, TimestampMixin): """Saved customer payment methods.""" __tablename__ = "payment_methods" id = Column(Integer, primary_key=True, index=True) store_id = Column(Integer, ForeignKey("stores.id"), nullable=False) customer_id = Column(Integer, ForeignKey("customers.id"), nullable=False) # Stripe payment method details stripe_payment_method_id = Column(String(255), nullable=False, index=True) payment_method_type = Column(String(50), nullable=False) # card, sepa_debit, etc. # Card details (if applicable) card_brand = Column(String(50)) # visa, mastercard, etc. card_last4 = Column(String(4)) card_exp_month = Column(Integer) card_exp_year = Column(Integer) # Settings is_default = Column(Boolean, default=False) is_active = Column(Boolean, default=True) # Relationships store = relationship("Store") customer = relationship("Customer") def __repr__(self): return f"" ``` ### Updated Order Model ```python # Update models/database/order.py class Order(Base, TimestampMixin): # ... existing fields ... # Payment integration payment_status = Column(String(50), default="pending") # pending, paid, failed, refunded payment_intent_id = Column(String(255)) # Stripe PaymentIntent ID total_amount_cents = Column(Integer, nullable=False) # Amount in cents # Relationships payment = relationship("Payment", back_populates="order", uselist=False) @property def total_amount_euros(self): """Convert cents to euros for display.""" return self.total_amount_cents / 100 if self.total_amount_cents else 0 ``` ## Payment Service Integration ### Stripe Service ```python # services/payment_service.py import stripe import json import logging from decimal import Decimal from typing import Dict, Optional from sqlalchemy.orm import Session from app.core.config import settings from models.database.payment import Payment, StorePaymentConfig from models.database.order import Order from models.database.store import Store from app.exceptions.payment import * logger = logging.getLogger(__name__) # Configure Stripe stripe.api_key = settings.stripe_secret_key class PaymentService: """Service for handling Stripe payments in multi-tenant environment.""" def __init__(self, db: Session): self.db = db def create_payment_intent( self, store_id: int, order_id: int, amount_euros: Decimal, customer_email: str, metadata: Optional[Dict] = None ) -> Dict: """Create Stripe PaymentIntent for store order.""" # Get store payment configuration payment_config = self.get_store_payment_config(store_id) if not payment_config.accepts_payments: raise PaymentNotConfiguredException(f"Store {store_id} not configured for payments") # Calculate amounts amount_cents = int(amount_euros * 100) platform_fee_cents = int(amount_cents * (payment_config.platform_fee_percentage / 100)) store_amount_cents = amount_cents - platform_fee_cents try: # Create PaymentIntent with Stripe Connect payment_intent = stripe.PaymentIntent.create( amount=amount_cents, currency=payment_config.currency.lower(), application_fee_amount=platform_fee_cents, transfer_data={ 'destination': payment_config.stripe_account_id, }, metadata={ 'store_id': str(store_id), 'order_id': str(order_id), 'platform': 'multi_tenant_ecommerce', **(metadata or {}) }, receipt_email=customer_email, description=f"Order payment for store {store_id}" ) # Create payment record payment = Payment( store_id=store_id, order_id=order_id, customer_id=self.get_order_customer_id(order_id), stripe_payment_intent_id=payment_intent.id, amount_total=amount_cents, amount_store=store_amount_cents, amount_platform_fee=platform_fee_cents, currency=payment_config.currency, status='pending', stripe_metadata=json.dumps(payment_intent.metadata) ) self.db.add(payment) # Update order order = self.db.query(Order).filter(Order.id == order_id).first() if order: order.payment_intent_id = payment_intent.id order.payment_status = 'pending' self.db.commit() return { 'payment_intent_id': payment_intent.id, 'client_secret': payment_intent.client_secret, 'amount_total': amount_euros, 'amount_store': store_amount_cents / 100, 'platform_fee': platform_fee_cents / 100, 'currency': payment_config.currency } except stripe.error.StripeError as e: logger.error(f"Stripe error creating PaymentIntent: {e}") raise PaymentProcessingException(f"Payment processing failed: {str(e)}") def confirm_payment(self, payment_intent_id: str) -> Payment: """Confirm payment and update records.""" try: # Retrieve PaymentIntent from Stripe payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) # Find payment record payment = self.db.query(Payment).filter( Payment.stripe_payment_intent_id == payment_intent_id ).first() if not payment: raise PaymentNotFoundException(f"Payment not found for intent {payment_intent_id}") # Update payment status based on Stripe status if payment_intent.status == 'succeeded': payment.status = 'succeeded' payment.stripe_charge_id = payment_intent.charges.data[0].id if payment_intent.charges.data else None payment.paid_at = datetime.utcnow() # Update order status order = self.db.query(Order).filter(Order.id == payment.order_id).first() if order: order.payment_status = 'paid' order.status = 'processing' # Move order to processing elif payment_intent.status == 'payment_failed': payment.status = 'failed' payment.failure_reason = payment_intent.last_payment_error.message if payment_intent.last_payment_error else "Unknown error" # Update order status order = self.db.query(Order).filter(Order.id == payment.order_id).first() if order: order.payment_status = 'failed' self.db.commit() return payment except stripe.error.StripeError as e: logger.error(f"Stripe error confirming payment: {e}") raise PaymentProcessingException(f"Payment confirmation failed: {str(e)}") def create_store_stripe_account(self, store_id: int, store_data: Dict) -> str: """Create Stripe Connect account for store.""" try: # Create Stripe Connect Express account account = stripe.Account.create( type='express', country='LU', # Luxembourg email=store_data.get('business_email'), capabilities={ 'card_payments': {'requested': True}, 'transfers': {'requested': True}, }, business_type='merchant', merchant={ 'name': store_data.get('business_name'), 'phone': store_data.get('business_phone'), 'address': { 'line1': store_data.get('address_line1'), 'city': store_data.get('city'), 'postal_code': store_data.get('postal_code'), 'country': 'LU' } }, metadata={ 'store_id': str(store_id), 'platform': 'multi_tenant_ecommerce' } ) # Update or create payment configuration payment_config = self.get_or_create_store_payment_config(store_id) payment_config.stripe_account_id = account.id payment_config.stripe_account_status = account.charges_enabled and account.payouts_enabled and 'active' or 'pending' self.db.commit() return account.id except stripe.error.StripeError as e: logger.error(f"Stripe error creating account: {e}") raise PaymentConfigurationException(f"Failed to create payment account: {str(e)}") def create_onboarding_link(self, store_id: int) -> str: """Create Stripe onboarding link for store.""" payment_config = self.get_store_payment_config(store_id) if not payment_config.stripe_account_id: raise PaymentNotConfiguredException("Store does not have Stripe account") try: account_link = stripe.AccountLink.create( account=payment_config.stripe_account_id, refresh_url=f"{settings.frontend_url}/store/admin/payments/refresh", return_url=f"{settings.frontend_url}/store/admin/payments/success", type='account_onboarding', ) # Update onboarding URL payment_config.stripe_onboarding_url = account_link.url self.db.commit() return account_link.url except stripe.error.StripeError as e: logger.error(f"Stripe error creating onboarding link: {e}") raise PaymentConfigurationException(f"Failed to create onboarding link: {str(e)}") def get_store_payment_config(self, store_id: int) -> StorePaymentConfig: """Get store payment configuration.""" config = self.db.query(StorePaymentConfig).filter( StorePaymentConfig.store_id == store_id ).first() if not config: raise PaymentNotConfiguredException(f"No payment configuration for store {store_id}") return config def webhook_handler(self, event_type: str, event_data: Dict) -> None: """Handle Stripe webhook events.""" if event_type == 'payment_intent.succeeded': payment_intent_id = event_data['object']['id'] self.confirm_payment(payment_intent_id) elif event_type == 'payment_intent.payment_failed': payment_intent_id = event_data['object']['id'] self.confirm_payment(payment_intent_id) elif event_type == 'account.updated': # Update store account status account_id = event_data['object']['id'] self.update_store_account_status(account_id, event_data['object']) # Add more webhook handlers as needed ``` ## API Endpoints ### Payment APIs ```python # app/api/v1/store/payments.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from app.core.database import get_db from middleware.store_context import require_store_context from models.database.store import Store from services.payment_service import PaymentService router = APIRouter(prefix="/payments", tags=["store-payments"]) @router.get("/config") async def get_payment_config( store: Store = Depends(require_store_context()), db: Session = Depends(get_db) ): """Get store payment configuration.""" payment_service = PaymentService(db) try: config = payment_service.get_store_payment_config(store.id) return { "stripe_account_id": config.stripe_account_id, "account_status": config.stripe_account_status, "accepts_payments": config.accepts_payments, "currency": config.currency, "platform_fee_percentage": float(config.platform_fee_percentage), "needs_onboarding": config.stripe_account_status != 'active' } except Exception: return { "stripe_account_id": None, "account_status": "not_configured", "accepts_payments": False, "needs_setup": True } @router.post("/setup") async def setup_payments( setup_data: dict, store: Store = Depends(require_store_context()), db: Session = Depends(get_db) ): """Set up Stripe payments for store.""" payment_service = PaymentService(db) store_data = { "business_name": store.name, "business_email": store.business_email, "business_phone": store.business_phone, **setup_data } account_id = payment_service.create_store_stripe_account(store.id, store_data) onboarding_url = payment_service.create_onboarding_link(store.id) return { "stripe_account_id": account_id, "onboarding_url": onboarding_url, "message": "Payment setup initiated. Complete onboarding to accept payments." } # app/api/v1/platform/stores/payments.py @router.post("/{store_id}/payments/create-intent") async def create_payment_intent( store_id: int, payment_data: dict, db: Session = Depends(get_db) ): """Create payment intent for customer order.""" payment_service = PaymentService(db) payment_intent = payment_service.create_payment_intent( store_id=store_id, order_id=payment_data['order_id'], amount_euros=Decimal(str(payment_data['amount'])), customer_email=payment_data['customer_email'], metadata=payment_data.get('metadata', {}) ) return payment_intent @router.post("/webhooks/stripe") async def stripe_webhook( request: Request, db: Session = Depends(get_db) ): """Handle Stripe webhook events.""" import stripe payload = await request.body() sig_header = request.headers.get('stripe-signature') try: event = stripe.Webhook.construct_event( payload, sig_header, settings.stripe_webhook_secret ) except ValueError: raise HTTPException(status_code=400, detail="Invalid payload") except stripe.error.SignatureVerificationError: raise HTTPException(status_code=400, detail="Invalid signature") payment_service = PaymentService(db) payment_service.webhook_handler(event['type'], event['data']) return {"status": "success"} ``` ## Frontend Integration ### Checkout Process ```javascript // frontend/js/storefront/checkout.js class CheckoutManager { constructor(storeId) { this.storeId = storeId; this.stripe = Stripe(STRIPE_PUBLISHABLE_KEY); this.elements = this.stripe.elements(); this.paymentElement = null; } async initializePayment(orderData) { // Create payment intent const response = await fetch(`/api/v1/platform/stores/${this.storeId}/payments/create-intent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order_id: orderData.orderId, amount: orderData.total, customer_email: orderData.customerEmail }) }); const { client_secret, amount_total, platform_fee } = await response.json(); // Display payment breakdown this.displayPaymentBreakdown(amount_total, platform_fee); // Create payment element this.paymentElement = this.elements.create('payment', { clientSecret: client_secret }); this.paymentElement.mount('#payment-element'); } async confirmPayment(orderData) { const { error } = await this.stripe.confirmPayment({ elements: this.elements, confirmParams: { return_url: `${window.location.origin}/storefront/order-confirmation`, receipt_email: orderData.customerEmail } }); if (error) { this.showPaymentError(error.message); } } } ``` ## Updated Workflow Integration ### Enhanced Customer Purchase Workflow ``` Customer adds products to cart ↓ Customer proceeds to checkout ↓ System creates Order (payment_status: pending) ↓ Frontend calls POST /api/v1/platform/stores/{store_id}/payments/create-intent ↓ PaymentService creates Stripe PaymentIntent with store destination ↓ Customer completes payment with Stripe Elements ↓ Stripe webhook confirms payment ↓ PaymentService updates Order (payment_status: paid, status: processing) ↓ Store receives order for fulfillment ``` ### Payment Configuration Workflow ``` Store accesses payment settings ↓ POST /api/v1/store/payments/setup ↓ System creates Stripe Connect account ↓ Store completes Stripe onboarding ↓ Webhook updates account status to 'active' ↓ Store can now accept payments ``` This integration provides secure, compliant payment processing while maintaining store isolation and enabling proper revenue distribution between stores and the platform.