29 KiB
29 KiB
🚀 Service Integration: Complete Implementation Guide
📋 Overview
The Project Mycelium has a complete app workflow but incomplete service workflow. This document provides everything needed to complete the service integration, making services purchasable through the marketplace with full tracking for both providers and customers.
🎯 Current State Analysis
✅ App Workflow - COMPLETE
- Creation:
DashboardController::add_app_api
→UserPersistence::add_user_app
- Marketplace:
applications_page
aggregates apps from all users - Purchase:
OrderService::checkout
→create_app_deployments_from_order
- Tracking:
AppDeployment
records for both provider and customer
🔄 Service Workflow - PARTIALLY COMPLETE
- Creation:
DashboardController::create_service
→UserPersistence::add_user_service
✅ - Marketplace:
services_page
aggregates services from all users ✅ - Purchase: ❌ MISSING - No service booking creation in OrderService
- Tracking: ❌ MISSING - No customer service booking records
The Gap
Services were designed for manual requests while apps were designed for automated marketplace purchases. We need to add service booking creation to the OrderService to mirror the app workflow.
🏗️ Implementation Plan
Phase 1: Data Models (2-3 hours)
1.1 Add ServiceBooking Model
// src/models/user.rs - Add after ServiceRequest struct
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceBooking {
pub id: String, // Same as ServiceRequest.id for cross-reference
pub service_id: String, // Reference to original service
pub service_name: String,
pub provider_email: String, // Who provides the service
pub customer_email: String, // Who booked the service
pub budget: i32,
pub estimated_hours: i32,
pub status: String, // "Pending", "In Progress", "Completed"
pub requested_date: String,
pub priority: String,
pub description: Option<String>,
pub booking_date: String, // When customer booked
pub client_phone: Option<String>,
pub progress: Option<i32>,
pub completed_date: Option<String>,
}
// Add CustomerServiceData to MockUserData
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomerServiceData {
pub active_bookings: i32,
pub completed_bookings: i32,
pub total_spent: i32,
pub service_bookings: Vec<ServiceBooking>,
}
// Update MockUserData struct - add this field
pub struct MockUserData {
// ... existing fields ...
// NEW: Customer service data
#[serde(default)]
pub customer_service_data: Option<CustomerServiceData>,
}
1.2 Update UserPersistentData
// src/services/user_persistence.rs - Add to UserPersistentData struct
pub struct UserPersistentData {
// ... existing fields ...
// NEW: Customer service bookings
#[serde(default)]
pub service_bookings: Vec<ServiceBooking>,
}
1.3 Add ServiceBooking Builder
// src/models/builders.rs - Add ServiceBooking builder
#[derive(Default)]
pub struct ServiceBookingBuilder {
id: Option<String>,
service_id: Option<String>,
service_name: Option<String>,
provider_email: Option<String>,
customer_email: Option<String>,
budget: Option<i32>,
estimated_hours: Option<i32>,
status: Option<String>,
requested_date: Option<String>,
priority: Option<String>,
description: Option<String>,
booking_date: Option<String>,
}
impl ServiceBookingBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn id(mut self, id: &str) -> Self {
self.id = Some(id.to_string());
self
}
pub fn service_id(mut self, service_id: &str) -> Self {
self.service_id = Some(service_id.to_string());
self
}
pub fn service_name(mut self, service_name: &str) -> Self {
self.service_name = Some(service_name.to_string());
self
}
pub fn provider_email(mut self, provider_email: &str) -> Self {
self.provider_email = Some(provider_email.to_string());
self
}
pub fn customer_email(mut self, customer_email: &str) -> Self {
self.customer_email = Some(customer_email.to_string());
self
}
pub fn budget(mut self, budget: i32) -> Self {
self.budget = Some(budget);
self
}
pub fn estimated_hours(mut self, hours: i32) -> Self {
self.estimated_hours = Some(hours);
self
}
pub fn status(mut self, status: &str) -> Self {
self.status = Some(status.to_string());
self
}
pub fn requested_date(mut self, date: &str) -> Self {
self.requested_date = Some(date.to_string());
self
}
pub fn priority(mut self, priority: &str) -> Self {
self.priority = Some(priority.to_string());
self
}
pub fn description(mut self, description: Option<String>) -> Self {
self.description = description;
self
}
pub fn booking_date(mut self, date: &str) -> Self {
self.booking_date = Some(date.to_string());
self
}
pub fn build(self) -> Result<ServiceBooking, String> {
Ok(ServiceBooking {
id: self.id.ok_or("ID is required")?,
service_id: self.service_id.ok_or("Service ID is required")?,
service_name: self.service_name.ok_or("Service name is required")?,
provider_email: self.provider_email.ok_or("Provider email is required")?,
customer_email: self.customer_email.ok_or("Customer email is required")?,
budget: self.budget.unwrap_or(0),
estimated_hours: self.estimated_hours.unwrap_or(0),
status: self.status.unwrap_or_else(|| "Pending".to_string()),
requested_date: self.requested_date.unwrap_or_else(|| chrono::Utc::now().format("%Y-%m-%d").to_string()),
priority: self.priority.unwrap_or_else(|| "Medium".to_string()),
description: self.description,
booking_date: self.booking_date.unwrap_or_else(|| chrono::Utc::now().format("%Y-%m-%d").to_string()),
client_phone: None,
progress: None,
completed_date: None,
})
}
}
impl ServiceBooking {
pub fn builder() -> ServiceBookingBuilder {
ServiceBookingBuilder::new()
}
}
Phase 2: Service Persistence (2-3 hours)
2.1 Add Service Booking Methods
// src/services/user_persistence.rs - Add these methods to impl UserPersistence
impl UserPersistence {
/// Add a service booking to customer's data
pub fn add_user_service_booking(
user_email: &str,
service_booking: ServiceBooking
) -> Result<(), Box<dyn std::error::Error>> {
let mut data = Self::load_user_data(user_email)
.unwrap_or_else(|| Self::create_default_user_data(user_email));
// Add service booking if not already present
if !data.service_bookings.iter().any(|b| b.id == service_booking.id) {
data.service_bookings.push(service_booking.clone());
log::info!("Added service booking '{}' to persistent data for user: {}", service_booking.id, user_email);
} else {
log::info!("Service booking '{}' already exists for user: {}", service_booking.id, user_email);
}
Self::save_user_data(user_email, &data)?;
Ok(())
}
/// Get customer's service bookings
pub fn get_user_service_bookings(user_email: &str) -> Vec<ServiceBooking> {
if let Some(data) = Self::load_user_data(user_email) {
data.service_bookings
} else {
Vec::new()
}
}
/// Convert ServiceRequest to ServiceBooking for customer
pub fn create_service_booking_from_request(
request: &ServiceRequest,
customer_email: &str,
provider_email: &str
) -> ServiceBooking {
ServiceBooking::builder()
.id(&request.id)
.service_id(&format!("svc_{}", &request.id[4..])) // Extract service ID from request ID
.service_name(&request.service_name)
.provider_email(provider_email)
.customer_email(customer_email)
.budget(request.budget)
.estimated_hours(request.estimated_hours)
.status(&request.status)
.requested_date(&request.requested_date)
.priority(&request.priority)
.description(request.description.clone())
.booking_date(&chrono::Utc::now().format("%Y-%m-%d").to_string())
.build()
.unwrap()
}
}
// Update create_default_user_data to include service_bookings
fn create_default_user_data(user_email: &str) -> UserPersistentData {
UserPersistentData {
// ... existing fields ...
service_bookings: Vec::default(), // Add this line
// ... rest of fields ...
}
}
Phase 3: Order Service Enhancement (4-5 hours)
3.1 Add Service Booking Creation
// src/services/order.rs - Add this method to impl OrderService
impl OrderService {
/// Create service bookings when services are successfully ordered
fn create_service_bookings_from_order(&self, order: &Order) -> Result<(), String> {
use crate::services::user_persistence::{UserPersistence, ServiceBooking};
use crate::models::user::{ServiceRequest, ServiceRequestBuilder};
use chrono::Utc;
log::info!("Creating service bookings for order: {}", order.id);
// Get customer information from order
let customer_email = order.user_id.clone();
let customer_name = if customer_email == "guest" {
"Guest User".to_string()
} else {
// Try to get customer name from persistent data
if let Some(customer_data) = UserPersistence::load_user_data(&customer_email) {
customer_data.name.unwrap_or_else(|| customer_email.clone())
} else {
customer_email.clone()
}
};
// Process each order item
for item in &order.items {
// Only create bookings for service products
if item.product_category == "service" {
log::info!("Creating booking for service: {} (Product ID: {})", item.product_name, item.product_id);
// Find the service provider by looking up who published this service
if let Some(service_provider_email) = self.find_service_provider(&item.product_id) {
log::info!("Found service provider: {} for service: {}", service_provider_email, item.product_name);
// Create service request for provider (same as existing pattern)
for _i in 0..item.quantity {
let request_id = format!("req-{}-{}",
&order.id[..8],
&uuid::Uuid::new_v4().to_string()[..8]
);
let service_request = ServiceRequest {
id: request_id.clone(),
client_name: customer_name.clone(),
client_email: Some(customer_email.clone()),
service_name: item.product_name.clone(),
status: "Pending".to_string(),
requested_date: Utc::now().format("%Y-%m-%d").to_string(),
estimated_hours: (item.unit_price_base.to_f64().unwrap_or(0.0) / 75.0) as i32, // Assume $75/hour
budget: item.unit_price_base.to_i32().unwrap_or(0),
priority: "Medium".to_string(),
description: Some(format!("Service booking from marketplace order {}", order.id)),
created_date: Some(Utc::now().format("%Y-%m-%d").to_string()),
client_phone: None,
progress: None,
completed_date: None,
};
// Add request to service provider's data
if let Err(e) = UserPersistence::add_user_service_request(&service_provider_email, service_request.clone()) {
log::error!("Failed to add service request to provider {}: {}", service_provider_email, e);
} else {
log::info!("Successfully created service request {} for provider: {}", request_id, service_provider_email);
}
// Create service booking for customer
let service_booking = UserPersistence::create_service_booking_from_request(
&service_request,
&customer_email,
&service_provider_email
);
// Add booking to customer's data
if customer_email != "guest" {
if let Err(e) = UserPersistence::add_user_service_booking(&customer_email, service_booking) {
log::error!("Failed to add service booking to customer {}: {}", customer_email, e);
} else {
log::info!("Successfully added service booking {} to customer: {}", request_id, customer_email);
}
}
}
} else {
log::warn!("Could not find service provider for product: {}", item.product_id);
}
}
}
Ok(())
}
/// Find the service provider (user who published the service) by product ID
fn find_service_provider(&self, product_id: &str) -> Option<String> {
// Get all user data files and search for the service
let user_data_dir = std::path::Path::new("user_data");
if !user_data_dir.exists() {
return None;
}
if let Ok(entries) = std::fs::read_dir(user_data_dir) {
for entry in entries.flatten() {
if let Some(file_name) = entry.file_name().to_str() {
if file_name.ends_with(".json") {
// Extract email from filename
let user_email = file_name
.trim_end_matches(".json")
.replace("_at_", "@")
.replace("_", ".");
// Check if this user has the service
let user_services = UserPersistence::get_user_services(&user_email);
for service in user_services {
if service.id == product_id {
log::info!("Found service {} published by user: {}", product_id, user_email);
return Some(user_email);
}
}
}
}
}
}
None
}
}
3.2 Update Checkout Method
// src/services/order.rs - Update the checkout method
pub fn checkout(&mut self, order_id: &str, payment_details: PaymentDetails) -> Result<PaymentResult, String> {
// ... existing payment processing code ...
// Update order with payment details
{
let order_storage = OrderStorage::instance();
let mut storage = order_storage.lock().unwrap();
if let Some(order) = storage.get_order_mut(order_id) {
if payment_result.success {
order.update_status(OrderStatus::Confirmed);
if let Some(payment_details) = &payment_result.payment_details {
order.set_payment_details(payment_details.clone());
}
// EXISTING: Create app deployments for successful app orders
if let Err(e) = self.create_app_deployments_from_order(&order) {
log::error!("Failed to create app deployments for order {}: {}", order_id, e);
}
// NEW: Create service bookings for successful service orders
if let Err(e) = self.create_service_bookings_from_order(&order) {
log::error!("Failed to create service bookings for order {}: {}", order_id, e);
}
} else {
order.update_status(OrderStatus::Failed);
}
}
}
Ok(payment_result)
}
Phase 4: Dashboard Integration (3-4 hours)
4.1 Add Service Booking API
// src/controllers/dashboard.rs - Add these methods to impl DashboardController
impl DashboardController {
/// Get user's service bookings for user dashboard
pub async fn get_user_service_bookings_api(session: Session) -> Result<impl Responder> {
log::info!("Getting user service bookings");
let user_email = match session.get::<String>("user_email") {
Ok(Some(email)) => email,
_ => {
return Ok(HttpResponse::Unauthorized().json(serde_json::json!({
"success": false,
"message": "User not authenticated"
})));
}
};
let service_bookings = UserPersistence::get_user_service_bookings(&user_email);
log::info!("Returning {} service bookings for user {}", service_bookings.len(), user_email);
Ok(HttpResponse::Ok().json(serde_json::json!({
"success": true,
"bookings": service_bookings
})))
}
}
4.2 Update User Data Loading
// src/controllers/dashboard.rs - Update load_user_with_mock_data method
fn load_user_with_mock_data(session: &Session) -> Option<User> {
// ... existing code ...
// Apply persistent data
if let Some(persistent_data) = UserPersistence::load_user_data(&user_email) {
// ... existing service provider data loading ...
// NEW: Load customer service bookings for user dashboard
if let Some(ref mut mock_data) = user.mock_data {
// Initialize customer service data if needed
if mock_data.customer_service_data.is_none() {
mock_data.customer_service_data = Some(CustomerServiceData {
active_bookings: 0,
completed_bookings: 0,
total_spent: 0,
service_bookings: Vec::new(),
});
}
// Load service bookings
if let Some(ref mut customer_data) = mock_data.customer_service_data {
customer_data.service_bookings = persistent_data.service_bookings.clone();
customer_data.active_bookings = customer_data.service_bookings.iter()
.filter(|b| b.status == "In Progress" || b.status == "Pending")
.count() as i32;
customer_data.completed_bookings = customer_data.service_bookings.iter()
.filter(|b| b.status == "Completed")
.count() as i32;
customer_data.total_spent = customer_data.service_bookings.iter()
.map(|b| b.budget)
.sum();
}
}
}
Some(user)
}
4.3 Add Route Configuration
// src/routes/mod.rs - Add this route to the dashboard routes
.route("/dashboard/service-bookings", web::get().to(DashboardController::get_user_service_bookings_api))
Phase 5: Frontend Integration (2-3 hours)
5.1 Update User Dashboard HTML
<!-- src/views/dashboard/user.html - Add this section after applications section -->
<section class="service-bookings-section">
<div class="section-header">
<h3>My Service Bookings</h3>
<span class="section-count" id="service-bookings-count">0</span>
</div>
<div class="table-responsive">
<table id="service-bookings-table" class="table">
<thead>
<tr>
<th>Service</th>
<th>Provider</th>
<th>Status</th>
<th>Budget</th>
<th>Est. Hours</th>
<th>Requested</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
<div id="no-service-bookings" class="empty-state" style="display: none;">
<p>No service bookings yet. <a href="/marketplace/services">Browse services</a> to get started.</p>
</div>
</section>
5.2 Update Dashboard JavaScript
// src/static/js/dashboard.js - Add these methods to UserDashboard class
class UserDashboard {
// ... existing methods ...
async loadServiceBookings() {
try {
const response = await fetch('/dashboard/service-bookings');
const data = await response.json();
if (data.success) {
this.serviceBookings = data.bookings || [];
this.populateServiceBookingsTable();
this.updateServiceBookingsCount();
} else {
console.error('Failed to load service bookings:', data.message);
}
} catch (error) {
console.error('Error loading service bookings:', error);
}
}
populateServiceBookingsTable() {
const tableBody = document.querySelector('#service-bookings-table tbody');
const emptyState = document.getElementById('no-service-bookings');
if (!tableBody) return;
tableBody.innerHTML = '';
if (this.serviceBookings.length === 0) {
document.getElementById('service-bookings-table').style.display = 'none';
emptyState.style.display = 'block';
return;
}
document.getElementById('service-bookings-table').style.display = 'table';
emptyState.style.display = 'none';
this.serviceBookings.forEach(booking => {
const row = document.createElement('tr');
row.innerHTML = `
<td>
<div class="service-info">
<strong>${booking.service_name}</strong>
${booking.description ? `<br><small class="text-muted">${booking.description}</small>` : ''}
</div>
</td>
<td>${booking.provider_email}</td>
<td>
<span class="status-badge ${booking.status.toLowerCase().replace(' ', '-')}">
${booking.status}
</span>
</td>
<td>${booking.budget} TFP</td>
<td>${booking.estimated_hours}h</td>
<td>${booking.requested_date}</td>
<td>
<button onclick="viewServiceBooking('${booking.id}')" class="btn btn-sm btn-primary">
View Details
</button>
</td>
`;
tableBody.appendChild(row);
});
}
updateServiceBookingsCount() {
const countElement = document.getElementById('service-bookings-count');
if (countElement) {
countElement.textContent = this.serviceBookings.length;
}
}
// Update the init method to load service bookings
async init() {
// ... existing initialization ...
await this.loadServiceBookings();
// ... rest of initialization ...
}
}
// Add global function for viewing service booking details
function viewServiceBooking(bookingId) {
const booking = window.userDashboard.serviceBookings.find(b => b.id === bookingId);
if (booking) {
// Show service booking details modal
alert(`Service Booking Details:\n\nService: ${booking.service_name}\nProvider: ${booking.provider_email}\nStatus: ${booking.status}\nBudget: ${booking.budget} TFP`);
// TODO: Replace with proper modal implementation
}
}
5.3 Add CSS Styles
/* src/static/css/dashboard.css - Add these styles */
.service-bookings-section {
margin-top: 2rem;
}
.service-info strong {
color: #333;
}
.service-info small {
font-size: 0.85em;
line-height: 1.2;
}
.status-badge.pending {
background-color: #ffc107;
color: #000;
}
.status-badge.in-progress {
background-color: #17a2b8;
color: #fff;
}
.status-badge.completed {
background-color: #28a745;
color: #fff;
}
.empty-state {
text-align: center;
padding: 2rem;
color: #6c757d;
}
.empty-state a {
color: #007bff;
text-decoration: none;
}
.empty-state a:hover {
text-decoration: underline;
}
📋 Implementation Checklist
Phase 1: Data Models ✅
- Add
ServiceBooking
model tosrc/models/user.rs
- Add
CustomerServiceData
toMockUserData
- Add
service_bookings
field toUserPersistentData
- Add
ServiceBookingBuilder
tosrc/models/builders.rs
Phase 2: Service Persistence ✅
- Add
add_user_service_booking
method toUserPersistence
- Add
get_user_service_bookings
method toUserPersistence
- Add
create_service_booking_from_request
helper method - Update
create_default_user_data
to include service bookings
Phase 3: Order Service Enhancement ✅
- Add
create_service_bookings_from_order
method toOrderService
- Add
find_service_provider
method toOrderService
- Update
checkout
method to call service booking creation - Test service booking creation with order processing
Phase 4: Dashboard Integration ✅
- Add
get_user_service_bookings_api
endpoint toDashboardController
- Update
load_user_with_mock_data
to include service bookings - Add service booking route to routing configuration
- Test API endpoint returns correct data
Phase 5: Frontend Implementation ✅
- Add service bookings section to user dashboard template
- Add
loadServiceBookings
method to dashboard JavaScript - Add
populateServiceBookingsTable
method - Add CSS styles for service booking display
- Test frontend displays service bookings correctly
Phase 6: End-to-End Testing ✅
- Test complete service workflow: create → list → purchase → track
- Verify service bookings appear in customer dashboard
- Verify service requests appear in provider dashboard
- Test data consistency between provider and customer views
- Test error handling and edge cases
🎯 Success Criteria
Functional Requirements
- Complete Service Journey: Create → List → Book → Track ✅
- Data Consistency: Same booking ID in both provider and customer data ✅
- Dashboard Integration: Service bookings visible in user dashboard ✅
- Revenue Attribution: Correct revenue tracking for service providers ✅
Technical Requirements
- Pattern Consistency: Follows established builder and persistence patterns ✅
- Data Integrity: All service bookings persist correctly ✅
- Performance: Service booking creation doesn't impact order processing ✅
- Error Handling: Graceful handling of service booking failures ✅
🚀 Implementation Timeline
Total Estimated Time: 12-16 hours
- Day 1-2: Data Models (Phase 1) - 2-3 hours
- Day 2-3: Service Persistence (Phase 2) - 2-3 hours
- Day 3-4: Order Service Enhancement (Phase 3) - 4-5 hours
- Day 4-5: Dashboard Integration (Phase 4) - 3-4 hours
- Day 5: Frontend Implementation (Phase 5) - 2-3 hours
🏆 Expected Outcome
After implementation, the service workflow will be complete:
- Service providers create services (already working)
- Services appear in marketplace (already working)
- Customers can purchase services through checkout (new)
- Service requests automatically created for providers (new)
- Service bookings automatically created for customers (new)
- Both parties can track service progress in their dashboards (new)
This brings services to full feature parity with the existing app workflow, completing the marketplace functionality for alpha testing.