18 KiB
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.