Files
projectmycelium/docs/dev/design/archive/vision/parts/roadmap/payment-integration-spec.md
2025-09-01 21:37:01 -04:00

18 KiB

Payment Integration Technical Specification

Document Purpose: Detailed technical specification for Stripe payment integration and TFC credits system.

Last Updated: 2025-08-04
Status: Implementation Ready


Overview

This document outlines the technical implementation for integrating Stripe payments with the Project Mycelium TFC credits system, enabling seamless fiat-to-credits conversion and automated grid deployments.


Architecture Components

1. Stripe Payment Service

// src/services/stripe_service.rs
use stripe::{Client, PaymentIntent, CreatePaymentIntent, Currency, Webhook, EventType};
use serde::{Deserialize, Serialize};
use rust_decimal::Decimal;

#[derive(Debug, Clone)]
pub struct StripeService {
    client: Client,
    webhook_secret: String,
    publishable_key: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TFCPurchaseRequest {
    pub amount_usd: Decimal,
    pub user_id: String,
    pub return_url: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PaymentIntentResponse {
    pub client_secret: String,
    pub payment_intent_id: String,
    pub amount_cents: u64,
    pub tfc_amount: Decimal,
}

impl StripeService {
    pub fn new(secret_key: String, webhook_secret: String, publishable_key: String) -> Self {
        Self {
            client: Client::new(secret_key),
            webhook_secret,
            publishable_key,
        }
    }

    pub async fn create_tfc_purchase_intent(
        &self,
        request: TFCPurchaseRequest,
    ) -> Result<PaymentIntentResponse, StripeError> {
        let amount_cents = (request.amount_usd * Decimal::from(100)).to_u64().unwrap();
        
        let payment_intent = PaymentIntent::create(&self.client, CreatePaymentIntent {
            amount: amount_cents,
            currency: Currency::USD,
            metadata: [
                ("user_id".to_string(), request.user_id.clone()),
                ("tfc_amount".to_string(), request.amount_usd.to_string()),
                ("purpose".to_string(), "tfc_purchase".to_string()),
            ].iter().cloned().collect(),
            ..Default::default()
        }).await?;

        Ok(PaymentIntentResponse {
            client_secret: payment_intent.client_secret.unwrap(),
            payment_intent_id: payment_intent.id.to_string(),
            amount_cents,
            tfc_amount: request.amount_usd,
        })
    }

    pub async fn handle_webhook(
        &self,
        payload: &str,
        signature: &str,
    ) -> Result<WebhookResult, StripeError> {
        let event = Webhook::construct_event(payload, signature, &self.webhook_secret)?;
        
        match event.type_ {
            EventType::PaymentIntentSucceeded => {
                if let Ok(payment_intent) = serde_json::from_value::<PaymentIntent>(event.data.object) {
                    return Ok(WebhookResult::PaymentSucceeded {
                        user_id: payment_intent.metadata.get("user_id").cloned().unwrap_or_default(),
                        tfc_amount: payment_intent.metadata.get("tfc_amount")
                            .and_then(|s| s.parse().ok())
                            .unwrap_or_default(),
                        payment_intent_id: payment_intent.id.to_string(),
                    });
                }
            }
            EventType::PaymentIntentPaymentFailed => {
                // Handle failed payments
                return Ok(WebhookResult::PaymentFailed);
            }
            _ => {}
        }
        
        Ok(WebhookResult::Ignored)
    }
}

#[derive(Debug)]
pub enum WebhookResult {
    PaymentSucceeded {
        user_id: String,
        tfc_amount: Decimal,
        payment_intent_id: String,
    },
    PaymentFailed,
    Ignored,
}

2. TFC Credits Management

// src/services/tfc_service.rs
use rust_decimal::Decimal;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TFCTransaction {
    pub id: String,
    pub user_id: String,
    pub transaction_type: TFCTransactionType,
    pub amount: Decimal,
    pub balance_before: Decimal,
    pub balance_after: Decimal,
    pub description: String,
    pub metadata: serde_json::Value,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TFCTransactionType {
    Purchase,           // Stripe payment → TFC
    Deployment,         // TFC → Grid deployment
    Refund,            // Failed deployment refund
    Transfer,          // User-to-user transfer
    AdminAdjustment,   // Manual balance adjustment
}

pub struct TFCService {
    db: Arc<dyn Database>,
}

impl TFCService {
    pub async fn add_tfc_credits(
        &self,
        user_id: &str,
        amount: Decimal,
        payment_intent_id: &str,
    ) -> Result<TFCTransaction, TFCError> {
        let current_balance = self.get_user_balance(user_id).await?;
        let new_balance = current_balance + amount;
        
        let transaction = TFCTransaction {
            id: generate_transaction_id(),
            user_id: user_id.to_string(),
            transaction_type: TFCTransactionType::Purchase,
            amount,
            balance_before: current_balance,
            balance_after: new_balance,
            description: format!("TFC purchase via Stripe ({})", payment_intent_id),
            metadata: json!({
                "payment_intent_id": payment_intent_id,
                "stripe_amount_cents": (amount * Decimal::from(100)).to_u64(),
            }),
            created_at: Utc::now(),
        };
        
        // Atomic transaction: update balance + record transaction
        self.db.execute_transaction(|tx| {
            tx.update_user_balance(user_id, new_balance)?;
            tx.insert_tfc_transaction(&transaction)?;
            Ok(())
        }).await?;
        
        Ok(transaction)
    }

    pub async fn deduct_tfc_for_deployment(
        &self,
        user_id: &str,
        amount: Decimal,
        deployment_id: &str,
        service_name: &str,
    ) -> Result<TFCTransaction, TFCError> {
        let current_balance = self.get_user_balance(user_id).await?;
        
        if current_balance < amount {
            return Err(TFCError::InsufficientBalance {
                required: amount,
                available: current_balance,
            });
        }
        
        let new_balance = current_balance - amount;
        
        let transaction = TFCTransaction {
            id: generate_transaction_id(),
            user_id: user_id.to_string(),
            transaction_type: TFCTransactionType::Deployment,
            amount: -amount, // Negative for deduction
            balance_before: current_balance,
            balance_after: new_balance,
            description: format!("Deployment: {} ({})", service_name, deployment_id),
            metadata: json!({
                "deployment_id": deployment_id,
                "service_name": service_name,
            }),
            created_at: Utc::now(),
        };
        
        self.db.execute_transaction(|tx| {
            tx.update_user_balance(user_id, new_balance)?;
            tx.insert_tfc_transaction(&transaction)?;
            Ok(())
        }).await?;
        
        Ok(transaction)
    }

    pub async fn get_user_balance(&self, user_id: &str) -> Result<Decimal, TFCError> {
        self.db.get_user_tfc_balance(user_id).await
    }

    pub async fn get_transaction_history(
        &self,
        user_id: &str,
        limit: Option<u32>,
    ) -> Result<Vec<TFCTransaction>, TFCError> {
        self.db.get_user_tfc_transactions(user_id, limit.unwrap_or(50)).await
    }
}

3. Buy Now Implementation

// src/controllers/marketplace.rs
use actix_web::{web, HttpResponse, Result};
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize)]
pub struct BuyNowRequest {
    pub service_id: String,
    pub payment_method: PaymentMethod,
}

#[derive(Debug, Deserialize)]
pub enum PaymentMethod {
    TFC,           // Use existing TFC balance
    Stripe,        // Purchase TFC with credit card
}

#[derive(Debug, Serialize)]
pub struct BuyNowResponse {
    pub success: bool,
    pub action: BuyNowAction,
    pub message: String,
}

#[derive(Debug, Serialize)]
pub enum BuyNowAction {
    DeploymentStarted { deployment_id: String },
    PaymentRequired { payment_intent: PaymentIntentResponse },
    InsufficientFunds { required: Decimal, available: Decimal },
}

impl MarketplaceController {
    pub async fn buy_now(
        &self,
        request: web::Json<BuyNowRequest>,
        session: Session,
    ) -> Result<HttpResponse> {
        let user_id = self.get_user_id_from_session(&session)?;
        let service = self.get_service(&request.service_id).await?;
        
        match request.payment_method {
            PaymentMethod::TFC => {
                // Check TFC balance and proceed with deployment
                let balance = self.tfc_service.get_user_balance(&user_id).await?;
                
                if balance >= service.price_tfc {
                    // Sufficient balance - start deployment
                    let deployment_id = self.start_deployment(&user_id, &service).await?;
                    
                    Ok(HttpResponse::Ok().json(BuyNowResponse {
                        success: true,
                        action: BuyNowAction::DeploymentStarted { deployment_id },
                        message: "Deployment started successfully".to_string(),
                    }))
                } else {
                    // Insufficient balance
                    Ok(HttpResponse::Ok().json(BuyNowResponse {
                        success: false,
                        action: BuyNowAction::InsufficientFunds {
                            required: service.price_tfc,
                            available: balance,
                        },
                        message: "Insufficient TFC balance".to_string(),
                    }))
                }
            }
            PaymentMethod::Stripe => {
                // Create Stripe payment intent for TFC purchase
                let payment_intent = self.stripe_service.create_tfc_purchase_intent(
                    TFCPurchaseRequest {
                        amount_usd: service.price_tfc, // 1 TFC = 1 USD
                        user_id: user_id.clone(),
                        return_url: format!("/marketplace/service/{}/deploy", service.id),
                    }
                ).await?;
                
                Ok(HttpResponse::Ok().json(BuyNowResponse {
                    success: true,
                    action: BuyNowAction::PaymentRequired { payment_intent },
                    message: "Payment required to complete purchase".to_string(),
                }))
            }
        }
    }

    async fn start_deployment(
        &self,
        user_id: &str,
        service: &MarketplaceService,
    ) -> Result<String, MarketplaceError> {
        // 1. Deduct TFC from user balance
        let transaction = self.tfc_service.deduct_tfc_for_deployment(
            user_id,
            service.price_tfc,
            &generate_deployment_id(),
            &service.name,
        ).await?;
        
        // 2. Calculate marketplace commission
        let commission = service.price_tfc * Decimal::from_str("0.10")?; // 10%
        let grid_payment = service.price_tfc - commission;
        
        // 3. Queue deployment
        let deployment = Deployment {
            id: transaction.metadata["deployment_id"].as_str().unwrap().to_string(),
            user_id: user_id.to_string(),
            service_id: service.id.clone(),
            tfc_amount: service.price_tfc,
            commission_amount: commission,
            grid_payment_amount: grid_payment,
            status: DeploymentStatus::Queued,
            created_at: Utc::now(),
        };
        
        self.deployment_service.queue_deployment(deployment).await?;
        
        Ok(transaction.metadata["deployment_id"].as_str().unwrap().to_string())
    }
}

Frontend Integration

1. Buy Now Button Component

<!-- Buy Now Button with Payment Options -->
<div class="buy-now-section">
    <div class="price-display">
        <span class="price">{{service.price_tfc}} TFC</span>
        <span class="usd-equivalent">${{service.price_tfc}} USD</span>
    </div>
    
    <div class="payment-options">
        <button id="buyNowTFC" class="btn btn-primary btn-lg" 
                onclick="buyNowWithTFC('{{service.id}}')">
            <i class="bi bi-lightning-fill"></i>
            Buy Now with TFC
        </button>
        
        <button id="buyNowStripe" class="btn btn-outline-primary btn-lg"
                onclick="buyNowWithStripe('{{service.id}}')">
            <i class="bi bi-credit-card"></i>
            Buy with Credit Card
        </button>
    </div>
    
    <div class="balance-info">
        <small>Your TFC Balance: <span id="userTFCBalance">{{user.tfc_balance}}</span> TFC</small>
    </div>
</div>

2. JavaScript Payment Handling

// Buy Now with TFC Credits
async function buyNowWithTFC(serviceId) {
    try {
        const response = await fetch('/api/marketplace/buy-now', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                service_id: serviceId,
                payment_method: 'TFC'
            })
        });
        
        const result = await response.json();
        
        if (result.success) {
            switch (result.action.type) {
                case 'DeploymentStarted':
                    showSuccessToast('Deployment started! Redirecting to dashboard...');
                    setTimeout(() => {
                        window.location.href = `/dashboard/deployments/${result.action.deployment_id}`;
                    }, 2000);
                    break;
            }
        } else {
            switch (result.action.type) {
                case 'InsufficientFunds':
                    showInsufficientFundsModal(result.action.required, result.action.available);
                    break;
            }
        }
    } catch (error) {
        showErrorToast('Failed to process purchase');
    }
}

// Buy Now with Stripe
async function buyNowWithStripe(serviceId) {
    try {
        const response = await fetch('/api/marketplace/buy-now', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                service_id: serviceId,
                payment_method: 'Stripe'
            })
        });
        
        const result = await response.json();
        
        if (result.success && result.action.type === 'PaymentRequired') {
            // Initialize Stripe Elements
            const stripe = Stripe('pk_test_your_publishable_key');
            const elements = stripe.elements();
            
            // Show Stripe payment modal
            showStripePaymentModal(stripe, result.action.payment_intent.client_secret);
        }
    } catch (error) {
        showErrorToast('Failed to initialize payment');
    }
}

// Stripe Payment Modal
function showStripePaymentModal(stripe, clientSecret) {
    const modal = document.getElementById('stripePaymentModal');
    const elements = stripe.elements();
    
    const cardElement = elements.create('card');
    cardElement.mount('#card-element');
    
    document.getElementById('confirmPayment').onclick = async () => {
        const {error, paymentIntent} = await stripe.confirmCardPayment(clientSecret, {
            payment_method: {
                card: cardElement,
            }
        });
        
        if (error) {
            showErrorToast(error.message);
        } else {
            showSuccessToast('Payment successful! TFC credits added to your account.');
            // Refresh balance and redirect
            updateTFCBalance();
            modal.hide();
        }
    };
    
    new bootstrap.Modal(modal).show();
}

// Real-time TFC Balance Updates
function updateTFCBalance() {
    fetch('/api/user/tfc-balance')
        .then(response => response.json())
        .then(data => {
            document.getElementById('userTFCBalance').textContent = data.balance;
        });
}

Database Schema Updates

-- TFC Transactions Table
CREATE TABLE tfc_transactions (
    id VARCHAR(255) PRIMARY KEY,
    user_id VARCHAR(255) NOT NULL,
    transaction_type VARCHAR(50) NOT NULL,
    amount DECIMAL(15,2) NOT NULL,
    balance_before DECIMAL(15,2) NOT NULL,
    balance_after DECIMAL(15,2) NOT NULL,
    description TEXT,
    metadata JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    INDEX idx_user_id (user_id),
    INDEX idx_created_at (created_at),
    INDEX idx_transaction_type (transaction_type)
);

-- Deployments Table
CREATE TABLE deployments (
    id VARCHAR(255) PRIMARY KEY,
    user_id VARCHAR(255) NOT NULL,
    service_id VARCHAR(255) NOT NULL,
    tfc_amount DECIMAL(15,2) NOT NULL,
    commission_amount DECIMAL(15,2) NOT NULL,
    grid_payment_amount DECIMAL(15,2) NOT NULL,
    tft_amount DECIMAL(15,8),
    grid_deployment_id VARCHAR(255),
    status VARCHAR(50) NOT NULL,
    error_message TEXT,
    connection_details JSONB,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    
    INDEX idx_user_id (user_id),
    INDEX idx_status (status),
    INDEX idx_created_at (created_at)
);

-- Update Users Table
ALTER TABLE users ADD COLUMN tfc_balance DECIMAL(15,2) DEFAULT 0.00;

Configuration

# config/marketplace.toml
[stripe]
secret_key = "sk_test_..."
publishable_key = "pk_test_..."
webhook_secret = "whsec_..."

[marketplace]
commission_rate = 0.10  # 10%
tfc_usd_rate = 1.00     # 1 TFC = 1 USD

[threefold_grid]
api_endpoint = "https://gridproxy.grid.tf"
tft_wallet_mnemonic = "your wallet mnemonic"

This technical specification provides the complete implementation details for integrating Stripe payments with the TFC credits system, enabling seamless fiat-to-deployment automation in the Project Mycelium.