593 lines
18 KiB
Markdown
593 lines
18 KiB
Markdown
# Event-Driven Flow Implementation Specification
|
|
|
|
## Overview
|
|
|
|
This document provides the complete implementation specification for converting the blocking payment.rs architecture to an event-driven flow system using RhaiDispatcher.
|
|
|
|
## File Structure
|
|
|
|
```
|
|
src/dsl/src/
|
|
├── flow_manager.rs # New: FlowManager implementation
|
|
├── payment.rs # Modified: Non-blocking payment functions
|
|
└── lib.rs # Modified: Include flow_manager module
|
|
```
|
|
|
|
## 1. FlowManager Implementation
|
|
|
|
### File: `src/dsl/src/flow_manager.rs`
|
|
|
|
```rust
|
|
use rhai_dispatcher::{RhaiDispatcher, RhaiDispatcherBuilder, RhaiDispatcherError};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::collections::HashMap;
|
|
use serde_json;
|
|
use tokio::runtime::Runtime;
|
|
|
|
#[derive(Debug)]
|
|
pub enum FlowError {
|
|
DispatcherError(RhaiDispatcherError),
|
|
ConfigurationError(String),
|
|
SerializationError(serde_json::Error),
|
|
}
|
|
|
|
impl From<RhaiDispatcherError> for FlowError {
|
|
fn from(err: RhaiDispatcherError) -> Self {
|
|
FlowError::DispatcherError(err)
|
|
}
|
|
}
|
|
|
|
impl From<serde_json::Error> for FlowError {
|
|
fn from(err: serde_json::Error) -> Self {
|
|
FlowError::SerializationError(err)
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for FlowError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
FlowError::DispatcherError(e) => write!(f, "Dispatcher error: {}", e),
|
|
FlowError::ConfigurationError(e) => write!(f, "Configuration error: {}", e),
|
|
FlowError::SerializationError(e) => write!(f, "Serialization error: {}", e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for FlowError {}
|
|
|
|
#[derive(Clone)]
|
|
pub struct FlowManager {
|
|
dispatcher: RhaiDispatcher,
|
|
worker_id: String,
|
|
context_id: String,
|
|
}
|
|
|
|
impl FlowManager {
|
|
pub fn new(worker_id: String, context_id: String, redis_url: Option<String>) -> Result<Self, FlowError> {
|
|
let redis_url = redis_url.unwrap_or_else(|| "redis://127.0.0.1/".to_string());
|
|
|
|
let dispatcher = RhaiDispatcherBuilder::new()
|
|
.caller_id("stripe") // API responses come from Stripe
|
|
.worker_id(&worker_id)
|
|
.context_id(&context_id)
|
|
.redis_url(&redis_url)
|
|
.build()?;
|
|
|
|
Ok(Self {
|
|
dispatcher,
|
|
worker_id,
|
|
context_id,
|
|
})
|
|
}
|
|
|
|
pub async fn dispatch_response_script(&self, script_name: &str, data: &str) -> Result<(), FlowError> {
|
|
let script_content = format!(
|
|
r#"
|
|
// Auto-generated response script for {}
|
|
let response_data = `{}`;
|
|
let parsed_data = parse_json(response_data);
|
|
|
|
// Include the response script
|
|
eval_file("flows/{}.rhai");
|
|
"#,
|
|
script_name,
|
|
data.replace('`', r#"\`"#),
|
|
script_name
|
|
);
|
|
|
|
self.dispatcher
|
|
.new_play_request()
|
|
.worker_id(&self.worker_id)
|
|
.context_id(&self.context_id)
|
|
.script(&script_content)
|
|
.submit()
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn dispatch_error_script(&self, script_name: &str, error: &str) -> Result<(), FlowError> {
|
|
let script_content = format!(
|
|
r#"
|
|
// Auto-generated error script for {}
|
|
let error_data = `{}`;
|
|
let parsed_error = parse_json(error_data);
|
|
|
|
// Include the error script
|
|
eval_file("flows/{}.rhai");
|
|
"#,
|
|
script_name,
|
|
error.replace('`', r#"\`"#),
|
|
script_name
|
|
);
|
|
|
|
self.dispatcher
|
|
.new_play_request()
|
|
.worker_id(&self.worker_id)
|
|
.context_id(&self.context_id)
|
|
.script(&script_content)
|
|
.submit()
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
// Global flow manager instance
|
|
static FLOW_MANAGER: Mutex<Option<FlowManager>> = Mutex::new(None);
|
|
|
|
pub fn initialize_flow_manager(worker_id: String, context_id: String, redis_url: Option<String>) -> Result<(), FlowError> {
|
|
let manager = FlowManager::new(worker_id, context_id, redis_url)?;
|
|
let mut global_manager = FLOW_MANAGER.lock().unwrap();
|
|
*global_manager = Some(manager);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_flow_manager() -> Result<FlowManager, FlowError> {
|
|
let global_manager = FLOW_MANAGER.lock().unwrap();
|
|
global_manager.as_ref()
|
|
.ok_or_else(|| FlowError::ConfigurationError("Flow manager not initialized".to_string()))
|
|
.cloned()
|
|
}
|
|
|
|
// Async HTTP request function for Stripe API
|
|
pub async fn make_stripe_request(
|
|
config: &super::StripeConfig,
|
|
endpoint: &str,
|
|
form_data: &HashMap<String, String>
|
|
) -> Result<String, String> {
|
|
let url = format!("{}/{}", super::STRIPE_API_BASE, endpoint);
|
|
|
|
let response = config.client
|
|
.post(&url)
|
|
.basic_auth(&config.secret_key, None::<&str>)
|
|
.form(form_data)
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("HTTP request failed: {}", e))?;
|
|
|
|
let response_text = response.text().await
|
|
.map_err(|e| format!("Failed to read response: {}", e))?;
|
|
|
|
let json: serde_json::Value = serde_json::from_str(&response_text)
|
|
.map_err(|e| format!("Failed to parse JSON: {}", e))?;
|
|
|
|
if json.get("error").is_some() {
|
|
Err(response_text)
|
|
} else {
|
|
Ok(response_text)
|
|
}
|
|
}
|
|
```
|
|
|
|
## 2. Payment.rs Modifications
|
|
|
|
### Add Dependencies
|
|
|
|
Add to the top of `payment.rs`:
|
|
|
|
```rust
|
|
mod flow_manager;
|
|
use flow_manager::{get_flow_manager, initialize_flow_manager, make_stripe_request, FlowError};
|
|
use std::thread;
|
|
use tokio::runtime::Runtime;
|
|
```
|
|
|
|
### Add Flow Initialization Function
|
|
|
|
Add to the `rhai_payment_module`:
|
|
|
|
```rust
|
|
#[rhai_fn(name = "init_flows", return_raw)]
|
|
pub fn init_flows(worker_id: String, context_id: String) -> Result<String, Box<EvalAltResult>> {
|
|
initialize_flow_manager(worker_id, context_id, None)
|
|
.map_err(|e| format!("Failed to initialize flow manager: {:?}", e))?;
|
|
|
|
Ok("Flow manager initialized successfully".to_string())
|
|
}
|
|
|
|
#[rhai_fn(name = "init_flows_with_redis", return_raw)]
|
|
pub fn init_flows_with_redis(worker_id: String, context_id: String, redis_url: String) -> Result<String, Box<EvalAltResult>> {
|
|
initialize_flow_manager(worker_id, context_id, Some(redis_url))
|
|
.map_err(|e| format!("Failed to initialize flow manager: {:?}", e))?;
|
|
|
|
Ok("Flow manager initialized successfully".to_string())
|
|
}
|
|
```
|
|
|
|
### Helper Function for Stripe Config
|
|
|
|
Add helper function to get stripe config:
|
|
|
|
```rust
|
|
fn get_stripe_config() -> Result<StripeConfig, Box<EvalAltResult>> {
|
|
let registry = ASYNC_REGISTRY.lock().unwrap();
|
|
let registry = registry.as_ref().ok_or("Stripe not configured. Call configure_stripe() first.")?;
|
|
Ok(registry.stripe_config.clone())
|
|
}
|
|
```
|
|
|
|
### Convert Payment Intent Function
|
|
|
|
Replace the existing `create_payment_intent` function:
|
|
|
|
```rust
|
|
#[rhai_fn(name = "create", return_raw)]
|
|
pub fn create_payment_intent(intent: &mut RhaiPaymentIntent) -> Result<String, Box<EvalAltResult>> {
|
|
let form_data = prepare_payment_intent_data(intent);
|
|
|
|
// Get flow manager and stripe config
|
|
let flow_manager = get_flow_manager()
|
|
.map_err(|e| format!("Flow manager error: {:?}", e))?;
|
|
let stripe_config = get_stripe_config()?;
|
|
|
|
// Spawn background thread for HTTP request
|
|
thread::spawn(move || {
|
|
let rt = Runtime::new().expect("Failed to create runtime");
|
|
rt.block_on(async {
|
|
match make_stripe_request(&stripe_config, "payment_intents", &form_data).await {
|
|
Ok(response) => {
|
|
if let Err(e) = flow_manager.dispatch_response_script(
|
|
"new_create_payment_intent_response",
|
|
&response
|
|
).await {
|
|
eprintln!("Failed to dispatch response: {:?}", e);
|
|
}
|
|
}
|
|
Err(error) => {
|
|
if let Err(e) = flow_manager.dispatch_error_script(
|
|
"new_create_payment_intent_error",
|
|
&error
|
|
).await {
|
|
eprintln!("Failed to dispatch error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Return immediately with confirmation
|
|
Ok("payment_intent_request_dispatched".to_string())
|
|
}
|
|
```
|
|
|
|
### Convert Product Function
|
|
|
|
Replace the existing `create_product` function:
|
|
|
|
```rust
|
|
#[rhai_fn(name = "create", return_raw)]
|
|
pub fn create_product(product: &mut RhaiProduct) -> Result<String, Box<EvalAltResult>> {
|
|
let form_data = prepare_product_data(product);
|
|
|
|
// Get flow manager and stripe config
|
|
let flow_manager = get_flow_manager()
|
|
.map_err(|e| format!("Flow manager error: {:?}", e))?;
|
|
let stripe_config = get_stripe_config()?;
|
|
|
|
// Spawn background thread for HTTP request
|
|
thread::spawn(move || {
|
|
let rt = Runtime::new().expect("Failed to create runtime");
|
|
rt.block_on(async {
|
|
match make_stripe_request(&stripe_config, "products", &form_data).await {
|
|
Ok(response) => {
|
|
if let Err(e) = flow_manager.dispatch_response_script(
|
|
"new_create_product_response",
|
|
&response
|
|
).await {
|
|
eprintln!("Failed to dispatch response: {:?}", e);
|
|
}
|
|
}
|
|
Err(error) => {
|
|
if let Err(e) = flow_manager.dispatch_error_script(
|
|
"new_create_product_error",
|
|
&error
|
|
).await {
|
|
eprintln!("Failed to dispatch error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Return immediately with confirmation
|
|
Ok("product_request_dispatched".to_string())
|
|
}
|
|
```
|
|
|
|
### Convert Price Function
|
|
|
|
Replace the existing `create_price` function:
|
|
|
|
```rust
|
|
#[rhai_fn(name = "create", return_raw)]
|
|
pub fn create_price(price: &mut RhaiPrice) -> Result<String, Box<EvalAltResult>> {
|
|
let form_data = prepare_price_data(price);
|
|
|
|
// Get flow manager and stripe config
|
|
let flow_manager = get_flow_manager()
|
|
.map_err(|e| format!("Flow manager error: {:?}", e))?;
|
|
let stripe_config = get_stripe_config()?;
|
|
|
|
// Spawn background thread for HTTP request
|
|
thread::spawn(move || {
|
|
let rt = Runtime::new().expect("Failed to create runtime");
|
|
rt.block_on(async {
|
|
match make_stripe_request(&stripe_config, "prices", &form_data).await {
|
|
Ok(response) => {
|
|
if let Err(e) = flow_manager.dispatch_response_script(
|
|
"new_create_price_response",
|
|
&response
|
|
).await {
|
|
eprintln!("Failed to dispatch response: {:?}", e);
|
|
}
|
|
}
|
|
Err(error) => {
|
|
if let Err(e) = flow_manager.dispatch_error_script(
|
|
"new_create_price_error",
|
|
&error
|
|
).await {
|
|
eprintln!("Failed to dispatch error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Return immediately with confirmation
|
|
Ok("price_request_dispatched".to_string())
|
|
}
|
|
```
|
|
|
|
### Convert Subscription Function
|
|
|
|
Replace the existing `create_subscription` function:
|
|
|
|
```rust
|
|
#[rhai_fn(name = "create", return_raw)]
|
|
pub fn create_subscription(subscription: &mut RhaiSubscription) -> Result<String, Box<EvalAltResult>> {
|
|
let form_data = prepare_subscription_data(subscription);
|
|
|
|
// Get flow manager and stripe config
|
|
let flow_manager = get_flow_manager()
|
|
.map_err(|e| format!("Flow manager error: {:?}", e))?;
|
|
let stripe_config = get_stripe_config()?;
|
|
|
|
// Spawn background thread for HTTP request
|
|
thread::spawn(move || {
|
|
let rt = Runtime::new().expect("Failed to create runtime");
|
|
rt.block_on(async {
|
|
match make_stripe_request(&stripe_config, "subscriptions", &form_data).await {
|
|
Ok(response) => {
|
|
if let Err(e) = flow_manager.dispatch_response_script(
|
|
"new_create_subscription_response",
|
|
&response
|
|
).await {
|
|
eprintln!("Failed to dispatch response: {:?}", e);
|
|
}
|
|
}
|
|
Err(error) => {
|
|
if let Err(e) = flow_manager.dispatch_error_script(
|
|
"new_create_subscription_error",
|
|
&error
|
|
).await {
|
|
eprintln!("Failed to dispatch error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Return immediately with confirmation
|
|
Ok("subscription_request_dispatched".to_string())
|
|
}
|
|
```
|
|
|
|
### Convert Coupon Function
|
|
|
|
Replace the existing `create_coupon` function:
|
|
|
|
```rust
|
|
#[rhai_fn(name = "create", return_raw)]
|
|
pub fn create_coupon(coupon: &mut RhaiCoupon) -> Result<String, Box<EvalAltResult>> {
|
|
let form_data = prepare_coupon_data(coupon);
|
|
|
|
// Get flow manager and stripe config
|
|
let flow_manager = get_flow_manager()
|
|
.map_err(|e| format!("Flow manager error: {:?}", e))?;
|
|
let stripe_config = get_stripe_config()?;
|
|
|
|
// Spawn background thread for HTTP request
|
|
thread::spawn(move || {
|
|
let rt = Runtime::new().expect("Failed to create runtime");
|
|
rt.block_on(async {
|
|
match make_stripe_request(&stripe_config, "coupons", &form_data).await {
|
|
Ok(response) => {
|
|
if let Err(e) = flow_manager.dispatch_response_script(
|
|
"new_create_coupon_response",
|
|
&response
|
|
).await {
|
|
eprintln!("Failed to dispatch response: {:?}", e);
|
|
}
|
|
}
|
|
Err(error) => {
|
|
if let Err(e) = flow_manager.dispatch_error_script(
|
|
"new_create_coupon_error",
|
|
&error
|
|
).await {
|
|
eprintln!("Failed to dispatch error: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
// Return immediately with confirmation
|
|
Ok("coupon_request_dispatched".to_string())
|
|
}
|
|
```
|
|
|
|
## 3. Remove Old Blocking Code
|
|
|
|
### Remove from payment.rs:
|
|
|
|
1. **AsyncFunctionRegistry struct and implementation** - No longer needed
|
|
2. **ASYNC_REGISTRY static** - No longer needed
|
|
3. **AsyncRequest struct** - No longer needed
|
|
4. **async_worker_loop function** - No longer needed
|
|
5. **handle_stripe_request function** - Replaced by make_stripe_request in flow_manager
|
|
6. **make_request method** - No longer needed
|
|
|
|
### Keep in payment.rs:
|
|
|
|
1. **All struct definitions** (RhaiProduct, RhaiPrice, etc.)
|
|
2. **All builder methods** (name, amount, currency, etc.)
|
|
3. **All prepare_*_data functions**
|
|
4. **All getter functions**
|
|
5. **StripeConfig struct**
|
|
6. **configure_stripe function** (but remove AsyncFunctionRegistry creation)
|
|
|
|
## 4. Update Cargo.toml
|
|
|
|
Add to `src/dsl/Cargo.toml`:
|
|
|
|
```toml
|
|
[dependencies]
|
|
# ... existing dependencies ...
|
|
rhai_dispatcher = { path = "../dispatcher" }
|
|
```
|
|
|
|
## 5. Update lib.rs
|
|
|
|
Add to `src/dsl/src/lib.rs`:
|
|
|
|
```rust
|
|
pub mod flow_manager;
|
|
```
|
|
|
|
## 6. Flow Script Templates
|
|
|
|
Create directory structure:
|
|
```
|
|
flows/
|
|
├── new_create_payment_intent_response.rhai
|
|
├── new_create_payment_intent_error.rhai
|
|
├── new_create_product_response.rhai
|
|
├── new_create_product_error.rhai
|
|
├── new_create_price_response.rhai
|
|
├── new_create_price_error.rhai
|
|
├── new_create_subscription_response.rhai
|
|
├── new_create_subscription_error.rhai
|
|
├── new_create_coupon_response.rhai
|
|
└── new_create_coupon_error.rhai
|
|
```
|
|
|
|
### Example Flow Scripts
|
|
|
|
#### flows/new_create_payment_intent_response.rhai
|
|
```rhai
|
|
let payment_intent_id = parsed_data.id;
|
|
let status = parsed_data.status;
|
|
|
|
print(`✅ Payment Intent Created: ${payment_intent_id}`);
|
|
print(`Status: ${status}`);
|
|
|
|
// Continue the flow based on status
|
|
if status == "requires_payment_method" {
|
|
print("Payment method required - ready for frontend");
|
|
} else if status == "succeeded" {
|
|
print("Payment completed successfully!");
|
|
}
|
|
|
|
// Store the payment intent ID for later use
|
|
set_context("payment_intent_id", payment_intent_id);
|
|
set_context("payment_status", status);
|
|
```
|
|
|
|
#### flows/new_create_payment_intent_error.rhai
|
|
```rhai
|
|
let error_type = parsed_error.error.type;
|
|
let error_message = parsed_error.error.message;
|
|
|
|
print(`❌ Payment Intent Error: ${error_type}`);
|
|
print(`Message: ${error_message}`);
|
|
|
|
// Handle different error types
|
|
if error_type == "card_error" {
|
|
print("Card was declined - notify user");
|
|
} else if error_type == "rate_limit_error" {
|
|
print("Rate limited - retry later");
|
|
} else {
|
|
print("Unknown error - log for investigation");
|
|
}
|
|
|
|
// Store error details for debugging
|
|
set_context("last_error_type", error_type);
|
|
set_context("last_error_message", error_message);
|
|
```
|
|
|
|
## 7. Usage Example
|
|
|
|
### main.rhai
|
|
```rhai
|
|
// Initialize the flow system
|
|
init_flows("worker-1", "context-123");
|
|
|
|
// Configure Stripe
|
|
configure_stripe("sk_test_...");
|
|
|
|
// Create payment intent (non-blocking)
|
|
let payment_intent = new_payment_intent()
|
|
.amount(2000)
|
|
.currency("usd")
|
|
.customer("cus_customer123");
|
|
|
|
let result = payment_intent.create();
|
|
print(`Request dispatched: ${result}`);
|
|
|
|
// Script ends here, but flow continues in background
|
|
// Response will trigger new_create_payment_intent_response.rhai
|
|
```
|
|
|
|
## 8. Testing Strategy
|
|
|
|
1. **Unit Tests**: Test FlowManager initialization and script dispatch
|
|
2. **Integration Tests**: Test full payment flow with mock Stripe responses
|
|
3. **Load Tests**: Verify non-blocking behavior under concurrent requests
|
|
4. **Error Tests**: Verify error flow handling and script dispatch
|
|
|
|
## 9. Migration Checklist
|
|
|
|
- [ ] Create flow_manager.rs with FlowManager implementation
|
|
- [ ] Add flow_manager module to lib.rs
|
|
- [ ] Update Cargo.toml with rhai_dispatcher dependency
|
|
- [ ] Modify payment.rs to remove blocking code
|
|
- [ ] Add flow initialization functions
|
|
- [ ] Convert all create functions to non-blocking pattern
|
|
- [ ] Create flow script templates
|
|
- [ ] Test basic payment intent flow
|
|
- [ ] Test error handling flows
|
|
- [ ] Verify non-blocking behavior
|
|
- [ ] Update documentation
|
|
|
|
This specification provides a complete roadmap for implementing the event-driven flow architecture using RhaiDispatcher. |