init projectmycelium
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
# 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
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```html
|
||||
<!-- 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
|
||||
|
||||
```javascript
|
||||
// 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
|
||||
|
||||
```sql
|
||||
-- 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
|
||||
|
||||
```toml
|
||||
# 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.
|
Reference in New Issue
Block a user