1use chrono::{DateTime, Utc};
2use roboticus_core::{Result, RoboticusError};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use tracing::{debug, info, warn};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ServiceDefinition {
10 pub id: String,
11 pub name: String,
12 pub description: String,
13 pub price_usdc: f64,
14 #[serde(default)]
15 pub capabilities_required: Vec<String>,
16 #[serde(default)]
17 pub max_concurrent: usize,
18 #[serde(default)]
19 pub estimated_duration_seconds: u64,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ServiceRequest {
25 pub id: String,
26 pub service_id: String,
27 pub requester: String,
28 pub parameters: serde_json::Value,
29 pub status: ServiceStatus,
30 pub payment_tx: Option<String>,
31 pub created_at: DateTime<Utc>,
32 pub completed_at: Option<DateTime<Utc>>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37pub enum ServiceStatus {
38 Quoted,
39 PaymentPending,
40 PaymentVerified,
41 InProgress,
42 Completed,
43 Failed,
44 Refunded,
45}
46
47pub struct ServiceManager {
49 catalog: HashMap<String, ServiceDefinition>,
50 requests: HashMap<String, ServiceRequest>,
51 request_counter: u64,
52}
53
54impl ServiceManager {
55 pub fn new() -> Self {
56 Self {
57 catalog: HashMap::new(),
58 requests: HashMap::new(),
59 request_counter: 0,
60 }
61 }
62
63 pub fn register_service(&mut self, service: ServiceDefinition) -> Result<()> {
65 if service.id.is_empty() {
66 return Err(RoboticusError::Config("service id cannot be empty".into()));
67 }
68 if service.price_usdc < 0.0 {
69 return Err(RoboticusError::Config("price cannot be negative".into()));
70 }
71 info!(id = %service.id, name = %service.name, price = service.price_usdc, "registered service");
72 self.catalog.insert(service.id.clone(), service);
73 Ok(())
74 }
75
76 pub fn get_service(&self, service_id: &str) -> Option<&ServiceDefinition> {
78 self.catalog.get(service_id)
79 }
80
81 pub fn list_services(&self) -> Vec<&ServiceDefinition> {
83 self.catalog.values().collect()
84 }
85
86 pub fn create_quote(
88 &mut self,
89 service_id: &str,
90 requester: &str,
91 params: serde_json::Value,
92 ) -> Result<ServiceRequest> {
93 let service = self
94 .catalog
95 .get(service_id)
96 .ok_or_else(|| RoboticusError::Config(format!("service '{}' not found", service_id)))?;
97
98 self.request_counter += 1;
99 let request_id = format!("req_{}", self.request_counter);
100
101 let request = ServiceRequest {
102 id: request_id.clone(),
103 service_id: service_id.to_string(),
104 requester: requester.to_string(),
105 parameters: params,
106 status: ServiceStatus::Quoted,
107 payment_tx: None,
108 created_at: Utc::now(),
109 completed_at: None,
110 };
111
112 info!(
113 request_id = %request.id,
114 service = %service.name,
115 price = service.price_usdc,
116 "created service quote"
117 );
118
119 self.requests.insert(request_id, request.clone());
120 Ok(request)
121 }
122
123 pub fn record_payment(&mut self, request_id: &str, tx_hash: &str) -> Result<()> {
125 let request = self
126 .requests
127 .get_mut(request_id)
128 .ok_or_else(|| RoboticusError::Config(format!("request '{}' not found", request_id)))?;
129
130 if request.status != ServiceStatus::Quoted {
131 return Err(RoboticusError::Config(format!(
132 "request '{}' is not in quoted state",
133 request_id
134 )));
135 }
136
137 request.payment_tx = Some(tx_hash.to_string());
138 request.status = ServiceStatus::PaymentVerified;
139 info!(request_id, tx = tx_hash, "payment recorded");
140 Ok(())
141 }
142
143 pub fn start_fulfillment(&mut self, request_id: &str) -> Result<()> {
145 let request = self
146 .requests
147 .get_mut(request_id)
148 .ok_or_else(|| RoboticusError::Config(format!("request '{}' not found", request_id)))?;
149
150 if request.status != ServiceStatus::PaymentVerified {
151 return Err(RoboticusError::Config(format!(
152 "request '{}' payment not verified",
153 request_id
154 )));
155 }
156
157 request.status = ServiceStatus::InProgress;
158 debug!(request_id, "fulfillment started");
159 Ok(())
160 }
161
162 pub fn complete_fulfillment(&mut self, request_id: &str) -> Result<()> {
164 let request = self
165 .requests
166 .get_mut(request_id)
167 .ok_or_else(|| RoboticusError::Config(format!("request '{}' not found", request_id)))?;
168
169 if request.status != ServiceStatus::InProgress {
170 return Err(RoboticusError::Config(format!(
171 "request '{}' is not in progress",
172 request_id
173 )));
174 }
175
176 request.status = ServiceStatus::Completed;
177 request.completed_at = Some(Utc::now());
178 info!(request_id, "fulfillment completed");
179 Ok(())
180 }
181
182 pub fn fail_fulfillment(&mut self, request_id: &str) -> Result<()> {
184 let request = self
185 .requests
186 .get_mut(request_id)
187 .ok_or_else(|| RoboticusError::Config(format!("request '{}' not found", request_id)))?;
188
189 request.status = ServiceStatus::Failed;
190 warn!(request_id, "fulfillment failed");
191 Ok(())
192 }
193
194 pub fn get_request(&self, request_id: &str) -> Option<&ServiceRequest> {
196 self.requests.get(request_id)
197 }
198
199 pub fn requests_by_status(&self, status: ServiceStatus) -> Vec<&ServiceRequest> {
201 self.requests
202 .values()
203 .filter(|r| r.status == status)
204 .collect()
205 }
206
207 pub fn total_revenue(&self) -> f64 {
209 self.requests
210 .values()
211 .filter(|r| r.status == ServiceStatus::Completed)
212 .filter_map(|r| self.catalog.get(&r.service_id))
213 .map(|s| s.price_usdc)
214 .sum()
215 }
216
217 pub fn catalog_size(&self) -> usize {
218 self.catalog.len()
219 }
220
221 pub fn request_count(&self) -> usize {
222 self.requests.len()
223 }
224}
225
226impl Default for ServiceManager {
227 fn default() -> Self {
228 Self::new()
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn test_service() -> ServiceDefinition {
237 ServiceDefinition {
238 id: "code-review".into(),
239 name: "Code Review".into(),
240 description: "Automated code review".into(),
241 price_usdc: 5.0,
242 capabilities_required: vec!["coding".into()],
243 max_concurrent: 3,
244 estimated_duration_seconds: 300,
245 }
246 }
247
248 #[test]
249 fn register_and_list() {
250 let mut mgr = ServiceManager::new();
251 mgr.register_service(test_service()).unwrap();
252 assert_eq!(mgr.catalog_size(), 1);
253 assert!(mgr.get_service("code-review").is_some());
254 }
255
256 #[test]
257 fn reject_empty_id() {
258 let mut mgr = ServiceManager::new();
259 let mut svc = test_service();
260 svc.id = String::new();
261 assert!(mgr.register_service(svc).is_err());
262 }
263
264 #[test]
265 fn reject_negative_price() {
266 let mut mgr = ServiceManager::new();
267 let mut svc = test_service();
268 svc.price_usdc = -1.0;
269 assert!(mgr.register_service(svc).is_err());
270 }
271
272 #[test]
273 fn full_lifecycle() {
274 let mut mgr = ServiceManager::new();
275 mgr.register_service(test_service()).unwrap();
276
277 let quote = mgr
278 .create_quote("code-review", "client-1", serde_json::json!({}))
279 .unwrap();
280 assert_eq!(quote.status, ServiceStatus::Quoted);
281
282 mgr.record_payment("e.id, "0xabc123").unwrap();
283 assert_eq!(
284 mgr.get_request("e.id).unwrap().status,
285 ServiceStatus::PaymentVerified
286 );
287
288 mgr.start_fulfillment("e.id).unwrap();
289 assert_eq!(
290 mgr.get_request("e.id).unwrap().status,
291 ServiceStatus::InProgress
292 );
293
294 mgr.complete_fulfillment("e.id).unwrap();
295 let req = mgr.get_request("e.id).unwrap();
296 assert_eq!(req.status, ServiceStatus::Completed);
297 assert!(req.completed_at.is_some());
298 }
299
300 #[test]
301 fn invalid_state_transitions() {
302 let mut mgr = ServiceManager::new();
303 mgr.register_service(test_service()).unwrap();
304 let quote = mgr
305 .create_quote("code-review", "client", serde_json::json!({}))
306 .unwrap();
307
308 assert!(mgr.start_fulfillment("e.id).is_err());
309 assert!(mgr.complete_fulfillment("e.id).is_err());
310 }
311
312 #[test]
313 fn total_revenue() {
314 let mut mgr = ServiceManager::new();
315 mgr.register_service(test_service()).unwrap();
316
317 for i in 0..3 {
318 let q = mgr
319 .create_quote("code-review", &format!("client-{i}"), serde_json::json!({}))
320 .unwrap();
321 mgr.record_payment(&q.id, &format!("tx-{i}")).unwrap();
322 mgr.start_fulfillment(&q.id).unwrap();
323 if i < 2 {
324 mgr.complete_fulfillment(&q.id).unwrap();
325 }
326 }
327
328 assert!((mgr.total_revenue() - 10.0).abs() < f64::EPSILON);
329 }
330
331 #[test]
332 fn requests_by_status_filter() {
333 let mut mgr = ServiceManager::new();
334 mgr.register_service(test_service()).unwrap();
335
336 mgr.create_quote("code-review", "a", serde_json::json!({}))
337 .unwrap();
338 mgr.create_quote("code-review", "b", serde_json::json!({}))
339 .unwrap();
340
341 assert_eq!(mgr.requests_by_status(ServiceStatus::Quoted).len(), 2);
342 assert_eq!(mgr.requests_by_status(ServiceStatus::Completed).len(), 0);
343 }
344
345 #[test]
346 fn fail_fulfillment() {
347 let mut mgr = ServiceManager::new();
348 mgr.register_service(test_service()).unwrap();
349 let q = mgr
350 .create_quote("code-review", "c", serde_json::json!({}))
351 .unwrap();
352 mgr.record_payment(&q.id, "tx").unwrap();
353 mgr.start_fulfillment(&q.id).unwrap();
354 mgr.fail_fulfillment(&q.id).unwrap();
355 assert_eq!(
356 mgr.get_request(&q.id).unwrap().status,
357 ServiceStatus::Failed
358 );
359 }
360
361 #[test]
362 fn service_status_serde() {
363 for status in [
364 ServiceStatus::Quoted,
365 ServiceStatus::PaymentPending,
366 ServiceStatus::PaymentVerified,
367 ServiceStatus::InProgress,
368 ServiceStatus::Completed,
369 ServiceStatus::Failed,
370 ServiceStatus::Refunded,
371 ] {
372 let json = serde_json::to_string(&status).unwrap();
373 let back: ServiceStatus = serde_json::from_str(&json).unwrap();
374 assert_eq!(status, back);
375 }
376 }
377}