init projectmycelium
This commit is contained in:
642
src/controllers/rental.rs
Normal file
642
src/controllers/rental.rs
Normal file
@@ -0,0 +1,642 @@
|
||||
use actix_web::{web, Result, Responder};
|
||||
use actix_session::Session;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
use crate::utils::response_builder::ResponseBuilder;
|
||||
use crate::models::user::{User, Transaction, TransactionType, TransactionStatus};
|
||||
use crate::config::get_app_config;
|
||||
use crate::services::product::ProductService;
|
||||
use crate::services::user_persistence::UserPersistence;
|
||||
use chrono::Utc;
|
||||
|
||||
/// Controller for handling rental and purchase operations
|
||||
pub struct RentalController;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RentProductRequest {
|
||||
pub product_id: String,
|
||||
pub duration: String, // "monthly", "yearly", etc.
|
||||
#[serde(default)]
|
||||
pub duration_days: Option<u32>, // Number of days for the rental
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RentalResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub rental_id: Option<String>,
|
||||
pub transaction_id: Option<String>,
|
||||
}
|
||||
|
||||
impl RentalController {
|
||||
/// Rent a product
|
||||
pub async fn rent_product(
|
||||
product_id: web::Path<String>,
|
||||
request: Option<web::Json<RentProductRequest>>,
|
||||
session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
// Get user from session
|
||||
let user_email = match session.get::<String>("user_email")? {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return ResponseBuilder::unauthorized().json(RentalResponse {
|
||||
success: false,
|
||||
message: "User not authenticated".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Get user data
|
||||
let user_json = match session.get::<String>("user")? {
|
||||
Some(json) => json,
|
||||
None => {
|
||||
return ResponseBuilder::unauthorized().json(RentalResponse {
|
||||
success: false,
|
||||
message: "User data not found".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
let mut user: User = match serde_json::from_str(&user_json) {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Invalid user data".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Get product from ProductService
|
||||
let product_service = ProductService::new();
|
||||
let product = match product_service.get_product_by_id(&product_id) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return ResponseBuilder::not_found().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Product not found".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Load user persistent data and check if product is already rented
|
||||
let mut user_data = UserPersistence::load_user_data(&user_email)
|
||||
.unwrap_or_else(|| UserPersistence::create_default_user_data(&user_email));
|
||||
|
||||
// Check if product is already in active rentals
|
||||
let already_rented = user_data.slice_rentals.iter()
|
||||
.any(|rental| rental.slice_format == product_id.to_string());
|
||||
|
||||
if already_rented {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Product already rented by user".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
|
||||
// Check user balance from persistent data
|
||||
let user_balance = user_data.wallet_balance_usd;
|
||||
let rental_cost = product.base_price;
|
||||
|
||||
if user_balance < rental_cost {
|
||||
let required = rental_cost;
|
||||
let available = user_balance;
|
||||
let deficit = required - available;
|
||||
return ResponseBuilder::payment_required()
|
||||
.error_envelope(
|
||||
"INSUFFICIENT_FUNDS",
|
||||
"Insufficient balance",
|
||||
serde_json::json!({
|
||||
"currency": "USD",
|
||||
"wallet_balance_usd": available,
|
||||
"required_usd": required,
|
||||
"deficit_usd": deficit
|
||||
})
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Extract request body (required when mocks enabled)
|
||||
let req_data = match request {
|
||||
Some(r) => r.into_inner(),
|
||||
None => {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Missing or invalid request body".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Create rental and transaction
|
||||
let rental_id = uuid::Uuid::new_v4().to_string();
|
||||
let transaction_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let transaction = Transaction {
|
||||
id: transaction_id.clone(),
|
||||
user_id: user_email.clone(),
|
||||
transaction_type: TransactionType::Rental {
|
||||
rental_id: transaction_id.clone(),
|
||||
rental_type: "slice".to_string(),
|
||||
},
|
||||
amount: rental_cost,
|
||||
currency: Some("USD".to_string()),
|
||||
exchange_rate_usd: Some(rust_decimal::Decimal::ONE),
|
||||
amount_usd: Some(rental_cost),
|
||||
description: Some(format!("Rental of product {} for {}", product_id, req_data.duration)),
|
||||
reference_id: Some(format!("rental-{}", uuid::Uuid::new_v4())),
|
||||
metadata: None,
|
||||
timestamp: Utc::now(),
|
||||
status: TransactionStatus::Completed,
|
||||
};
|
||||
|
||||
// Update user persistent data
|
||||
// Deduct balance
|
||||
user_data.wallet_balance_usd -= rental_cost;
|
||||
|
||||
// Add transaction
|
||||
user_data.transactions.push(transaction);
|
||||
|
||||
// Create a slice rental record
|
||||
let slice_rental = crate::services::slice_calculator::SliceRental {
|
||||
rental_id: rental_id.clone(),
|
||||
slice_combination_id: format!("combo-{}", rental_id),
|
||||
node_id: "node-placeholder".to_string(), // TODO: Get from product
|
||||
farmer_email: "farmer@example.com".to_string(), // TODO: Get from product
|
||||
slice_allocation: crate::services::slice_calculator::SliceAllocation {
|
||||
allocation_id: format!("alloc-{}", rental_id),
|
||||
slice_combination_id: format!("combo-{}", rental_id),
|
||||
renter_email: user_email.clone(),
|
||||
base_slices_used: 1,
|
||||
rental_start: Utc::now(),
|
||||
rental_end: None,
|
||||
status: crate::services::slice_calculator::AllocationStatus::Active,
|
||||
monthly_cost: rental_cost,
|
||||
},
|
||||
total_cost: rental_cost,
|
||||
payment_status: crate::services::slice_calculator::PaymentStatus::Paid,
|
||||
id: rental_id.clone(),
|
||||
user_email: user_email.clone(),
|
||||
slice_format: "1x1".to_string(),
|
||||
status: "Active".to_string(),
|
||||
start_date: Some(Utc::now()),
|
||||
rental_duration_days: Some(30),
|
||||
monthly_cost: Some(rental_cost),
|
||||
deployment_type: Some("vm".to_string()),
|
||||
deployment_name: Some(format!("deployment-{}", rental_id)),
|
||||
deployment_config: None,
|
||||
deployment_status: Some("Provisioning".to_string()),
|
||||
deployment_endpoint: None,
|
||||
deployment_metadata: None,
|
||||
};
|
||||
user_data.slice_rentals.push(slice_rental);
|
||||
|
||||
// Add user activity
|
||||
user_data.user_activities.push(crate::models::user::UserActivity {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
user_email: user_email.clone(),
|
||||
activity_type: crate::models::user::ActivityType::SliceRental,
|
||||
description: format!("Rented {} for ${}", product.name, rental_cost),
|
||||
metadata: Some(serde_json::json!({
|
||||
"product_id": product_id.to_string(),
|
||||
"rental_id": rental_id,
|
||||
"cost": rental_cost
|
||||
})),
|
||||
timestamp: Utc::now(),
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
session_id: None,
|
||||
category: "Rental".to_string(),
|
||||
importance: crate::models::user::ActivityImportance::Medium,
|
||||
});
|
||||
|
||||
// Save updated user data
|
||||
UserPersistence::save_user_data(&user_data)?;
|
||||
|
||||
ResponseBuilder::ok().json(RentalResponse {
|
||||
success: true,
|
||||
message: format!("Successfully rented {} for ${}", product.name, rental_cost),
|
||||
rental_id: Some(rental_id),
|
||||
transaction_id: Some(transaction_id),
|
||||
}).build()
|
||||
}
|
||||
|
||||
/// Purchase a product (one-time payment)
|
||||
pub async fn purchase_product(
|
||||
product_id: web::Path<String>,
|
||||
session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
// Gate mock-based purchase when mocks are disabled
|
||||
if !get_app_config().enable_mock_data() {
|
||||
return ResponseBuilder::not_found().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Purchase feature unavailable".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
// Get user from session
|
||||
let user_email = match session.get::<String>("user_email")? {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return ResponseBuilder::unauthorized().json(RentalResponse {
|
||||
success: false,
|
||||
message: "User not authenticated".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Get user data
|
||||
let user_json = match session.get::<String>("user")? {
|
||||
Some(json) => json,
|
||||
None => {
|
||||
return ResponseBuilder::unauthorized().json(RentalResponse {
|
||||
success: false,
|
||||
message: "User data not found".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
let mut user: User = match serde_json::from_str(&user_json) {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Invalid user data".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Get product from ProductService (replaces MockDataService)
|
||||
let product_service = match crate::services::product::ProductService::builder().build() {
|
||||
Ok(service) => service,
|
||||
Err(_) => {
|
||||
return ResponseBuilder::internal_error().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Failed to initialize product service".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
let product = match product_service.get_product_by_id(&product_id) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
return ResponseBuilder::not_found().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Product not found".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if product is already owned by this user
|
||||
if let Ok(owned_products) = user.get_owned_products() {
|
||||
if owned_products.iter().any(|p| p.id == **product_id) {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Product already owned by user".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
}
|
||||
|
||||
// Continue with rental logic if not owned
|
||||
if false { // This condition will be replaced by the existing logic below
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Product already owned by user".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
|
||||
// Check user balance
|
||||
let user_balance = user.get_wallet_balance()?;
|
||||
let purchase_cost = product.base_price;
|
||||
|
||||
if user_balance < purchase_cost {
|
||||
let required = purchase_cost;
|
||||
let available = user_balance;
|
||||
let deficit = required - available;
|
||||
return ResponseBuilder::payment_required()
|
||||
.error_envelope(
|
||||
"INSUFFICIENT_FUNDS",
|
||||
"Insufficient balance",
|
||||
serde_json::json!({
|
||||
"currency": "USD",
|
||||
"wallet_balance_usd": available,
|
||||
"required_usd": required,
|
||||
"deficit_usd": deficit
|
||||
})
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Create transaction
|
||||
let transaction_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
let transaction = Transaction {
|
||||
id: transaction_id.clone(),
|
||||
user_id: user_email.clone(),
|
||||
transaction_type: TransactionType::Purchase {
|
||||
product_id: product_id.to_string(),
|
||||
},
|
||||
amount: purchase_cost,
|
||||
currency: Some("USD".to_string()),
|
||||
exchange_rate_usd: Some(rust_decimal::Decimal::ONE),
|
||||
amount_usd: Some(purchase_cost),
|
||||
description: Some(format!("Purchase of product {}", product_id)),
|
||||
reference_id: Some(format!("purchase-{}", uuid::Uuid::new_v4())),
|
||||
metadata: None,
|
||||
timestamp: Utc::now(),
|
||||
status: TransactionStatus::Completed,
|
||||
};
|
||||
|
||||
// Update user data using persistent data
|
||||
let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(&user_email);
|
||||
|
||||
// Deduct balance
|
||||
persistent_data.wallet_balance_usd -= purchase_cost;
|
||||
|
||||
// Add to owned products
|
||||
persistent_data.owned_product_ids.push(product_id.to_string());
|
||||
|
||||
// Add transaction
|
||||
persistent_data.transactions.push(transaction);
|
||||
|
||||
// Update user activities
|
||||
persistent_data.user_activities.insert(0, crate::models::user::UserActivity {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_email: user_email.clone(),
|
||||
activity_type: crate::models::user::ActivityType::Purchase,
|
||||
description: format!("Purchased {} for ${}", product.name, purchase_cost),
|
||||
timestamp: Utc::now(),
|
||||
metadata: None,
|
||||
category: "Purchase".to_string(),
|
||||
importance: crate::models::user::ActivityImportance::High,
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
session_id: None,
|
||||
});
|
||||
|
||||
// Save the updated persistent data
|
||||
if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data(&persistent_data) {
|
||||
log::error!("Failed to save user data after purchase: {}", e);
|
||||
}
|
||||
|
||||
// Update session with new user data
|
||||
let updated_user_json = serde_json::to_string(&user).unwrap();
|
||||
session.insert("user", updated_user_json)?;
|
||||
|
||||
ResponseBuilder::ok().json(RentalResponse {
|
||||
success: true,
|
||||
message: format!("Successfully purchased {} for ${}", product.name, purchase_cost),
|
||||
rental_id: None,
|
||||
transaction_id: Some(transaction_id),
|
||||
}).build()
|
||||
}
|
||||
|
||||
/// Cancel a rental
|
||||
pub async fn cancel_rental(
|
||||
rental_id: web::Path<String>,
|
||||
session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
// Gate mock-based rental cancel when mocks are disabled
|
||||
if !get_app_config().enable_mock_data() {
|
||||
return ResponseBuilder::not_found().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Rental feature unavailable".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
// Get user from session
|
||||
let user_email = match session.get::<String>("user_email")? {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return ResponseBuilder::unauthorized().json(RentalResponse {
|
||||
success: false,
|
||||
message: "User not authenticated".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Get user data
|
||||
let user_json = match session.get::<String>("user")? {
|
||||
Some(json) => json,
|
||||
None => {
|
||||
return ResponseBuilder::unauthorized().json(RentalResponse {
|
||||
success: false,
|
||||
message: "User data not found".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
let mut user: User = match serde_json::from_str(&user_json) {
|
||||
Ok(u) => u,
|
||||
Err(_) => {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Invalid user data".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Update user data using persistent data
|
||||
let user_email = &user.email;
|
||||
let mut persistent_data = crate::models::builders::SessionDataBuilder::load_or_create(user_email);
|
||||
|
||||
// Remove rental
|
||||
if let Some(pos) = persistent_data.active_product_rentals.iter().position(|x| x.rental_id == rental_id.to_string()) {
|
||||
persistent_data.active_product_rentals.remove(pos);
|
||||
|
||||
// Update user activities
|
||||
persistent_data.user_activities.insert(0, crate::models::user::UserActivity {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
user_email: user_email.clone(),
|
||||
activity_type: crate::models::user::ActivityType::Purchase,
|
||||
description: format!("Cancelled rental {}", rental_id),
|
||||
timestamp: Utc::now(),
|
||||
metadata: None,
|
||||
category: "Rental".to_string(),
|
||||
importance: crate::models::user::ActivityImportance::Medium,
|
||||
ip_address: None,
|
||||
user_agent: None,
|
||||
session_id: None,
|
||||
});
|
||||
|
||||
// Keep only last 10 activities
|
||||
if persistent_data.user_activities.len() > 10 {
|
||||
persistent_data.user_activities.truncate(10);
|
||||
}
|
||||
|
||||
// Save the updated persistent data
|
||||
if let Err(e) = crate::services::user_persistence::UserPersistence::save_user_data(&persistent_data) {
|
||||
log::error!("Failed to save user data after rental cancellation: {}", e);
|
||||
}
|
||||
} else {
|
||||
return ResponseBuilder::not_found().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Rental not found".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
|
||||
// Update session with new user data
|
||||
let updated_user_json = serde_json::to_string(&user).unwrap();
|
||||
session.insert("user", updated_user_json)?;
|
||||
|
||||
ResponseBuilder::ok().json(RentalResponse {
|
||||
success: true,
|
||||
message: "Rental cancelled successfully".to_string(),
|
||||
rental_id: Some(rental_id.to_string()),
|
||||
transaction_id: None,
|
||||
}).build()
|
||||
}
|
||||
|
||||
/// Rent a node product (slice or full node)
|
||||
pub async fn rent_node_product(
|
||||
product_id: web::Path<String>,
|
||||
request: Option<web::Json<RentNodeProductRequest>>,
|
||||
session: Session,
|
||||
) -> Result<impl Responder> {
|
||||
// Gate mock-based node rental when mocks are disabled
|
||||
if !get_app_config().enable_mock_data() {
|
||||
return ResponseBuilder::not_found().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Node rental feature unavailable".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
// Get user from session
|
||||
let user_email = match session.get::<String>("user_email")? {
|
||||
Some(email) => email,
|
||||
None => {
|
||||
return ResponseBuilder::unauthorized().json(RentalResponse {
|
||||
success: false,
|
||||
message: "User not authenticated".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize node rental service
|
||||
let node_rental_service = match crate::services::node_rental::NodeRentalService::builder()
|
||||
.auto_billing_enabled(true)
|
||||
.notification_enabled(true)
|
||||
.conflict_prevention(true)
|
||||
.build()
|
||||
{
|
||||
Ok(service) => service,
|
||||
Err(e) => {
|
||||
return ResponseBuilder::internal_error().json(RentalResponse {
|
||||
success: false,
|
||||
message: format!("Service initialization failed: {}", e),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Extract request body (required when mocks enabled)
|
||||
let req_data = match request {
|
||||
Some(r) => r.into_inner(),
|
||||
None => {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Missing or invalid request body".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Parse duration and rental type
|
||||
let duration_months = match req_data.duration.as_str() {
|
||||
"monthly" => 1,
|
||||
"quarterly" => 3,
|
||||
"yearly" => 12,
|
||||
_ => {
|
||||
return ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: "Invalid duration. Use 'monthly', 'quarterly', or 'yearly'".to_string(),
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build();
|
||||
}
|
||||
};
|
||||
|
||||
// Determine rental type and cost based on product
|
||||
let (rental_type, monthly_cost) = if product_id.starts_with("fullnode_") {
|
||||
(crate::models::user::NodeRentalType::FullNode, req_data.monthly_cost.unwrap_or_else(|| rust_decimal::Decimal::from(200)))
|
||||
} else {
|
||||
// For slice products, we'd need to get slice configuration
|
||||
// For now, use a default slice configuration
|
||||
(crate::models::user::NodeRentalType::Slice, req_data.monthly_cost.unwrap_or_else(|| rust_decimal::Decimal::from(50)))
|
||||
};
|
||||
|
||||
// Attempt to rent the node
|
||||
match node_rental_service.rent_node_product(
|
||||
&product_id,
|
||||
&user_email,
|
||||
duration_months,
|
||||
rental_type,
|
||||
monthly_cost,
|
||||
) {
|
||||
Ok((rental, _earning)) => {
|
||||
|
||||
ResponseBuilder::ok().json(RentalResponse {
|
||||
success: true,
|
||||
message: "Node rental successful".to_string(),
|
||||
rental_id: Some(rental.id),
|
||||
transaction_id: None,
|
||||
}).build()
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
ResponseBuilder::bad_request().json(RentalResponse {
|
||||
success: false,
|
||||
message: e,
|
||||
rental_id: None,
|
||||
transaction_id: None,
|
||||
}).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct RentNodeProductRequest {
|
||||
pub duration: String, // "monthly", "quarterly", "yearly"
|
||||
pub monthly_cost: Option<rust_decimal::Decimal>,
|
||||
}
|
||||
Reference in New Issue
Block a user