Add documentation and core BASIC language functions

- Add SET_SCHEDULE.md and TEMPLATE_VARIABLES.md documentation
- Implement array functions (CONTAINS, PUSH/POP, SLICE, SORT, UNIQUE)
- Implement math functions module structure
- Implement datetime functions module structure
- Implement validation functions (ISNULL, ISEMPTY, VAL, STR, TYPEOF)
- Implement error handling functions (THROW, ERROR, ASSERT)
- Add CRM lead scoring keywords (SCORE_LEAD, AI_SCORE_LEAD)
- Add messaging keywords (SEND_TEMPLATE, CREATE_TEMPLATE)
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-11-30 11:09:16 -03:00
parent 50eae38d36
commit a41ff7a7d4
28 changed files with 4811 additions and 257 deletions

230
docs/SET_SCHEDULE.md Normal file
View file

@ -0,0 +1,230 @@
# SET SCHEDULE Keyword Reference
> Documentation for scheduling scripts and automations in General Bots
## Overview
SET SCHEDULE has two distinct usages:
1. **File-level scheduler**: When placed at the top of a `.bas` file, defines when the script runs automatically
2. **Runtime scheduling**: When used within code, schedules another script to run at a specific time
## Usage 1: File-Level Scheduler
When SET SCHEDULE appears at the top of a file (before any executable code), it registers the entire script to run on a cron schedule.
### Syntax
```basic
SET SCHEDULE "cron_expression"
' Rest of script follows
TALK "This runs on schedule"
```
### Examples
```basic
' Run every day at 9 AM
SET SCHEDULE "0 9 * * *"
TALK "Good morning! Running daily report..."
stats = AGGREGATE "sales", "SUM", "amount", "date = TODAY()"
SEND TEMPLATE "daily-report", "email", "team@company.com", #{total: stats}
```
```basic
' Run every Monday at 10 AM
SET SCHEDULE "0 10 * * 1"
TALK "Weekly summary starting..."
' Generate weekly report
```
```basic
' Run first day of every month at midnight
SET SCHEDULE "0 0 1 * *"
TALK "Monthly billing cycle..."
' Process monthly billing
```
### Cron Expression Reference
```
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *
```
| Expression | Description |
|------------|-------------|
| `0 9 * * *` | Every day at 9:00 AM |
| `0 9 * * 1-5` | Weekdays at 9:00 AM |
| `0 */2 * * *` | Every 2 hours |
| `30 8 * * 1` | Mondays at 8:30 AM |
| `0 0 1 * *` | First of each month at midnight |
| `0 12 * * 0` | Sundays at noon |
| `*/15 * * * *` | Every 15 minutes |
| `0 9,17 * * *` | At 9 AM and 5 PM daily |
## Usage 2: Runtime Scheduling
When used within code (not at file top), schedules another script to run at a specified time.
### Syntax
```basic
' Schedule with cron + date
SET SCHEDULE "cron_expression" date, "script.bas"
' Schedule for specific date/time
SET SCHEDULE datetime, "script.bas"
```
### Examples
```basic
' Schedule script to run in 3 days at 9 AM
SET SCHEDULE "0 9 * * *" DATEADD(TODAY(), 3, "day"), "followup-email.bas"
```
```basic
' Schedule for specific datetime
SET SCHEDULE "2025-02-15 10:00", "product-launch.bas"
```
```basic
' Schedule relative to now
SET SCHEDULE DATEADD(NOW(), 1, "hour"), "reminder.bas"
SET SCHEDULE DATEADD(NOW(), 30, "minute"), "check-status.bas"
```
```basic
' Campaign scheduling example
ON FORM SUBMIT "signup"
' Welcome email immediately
SEND TEMPLATE "welcome", "email", fields.email, #{name: fields.name}
' Schedule follow-up sequence
SET SCHEDULE DATEADD(NOW(), 2, "day"), "nurture-day-2.bas"
SET SCHEDULE DATEADD(NOW(), 5, "day"), "nurture-day-5.bas"
SET SCHEDULE DATEADD(NOW(), 14, "day"), "nurture-day-14.bas"
END ON
```
## Passing Data to Scheduled Scripts
Use SET BOT MEMORY to pass data to scheduled scripts:
```basic
' In the scheduling script
SET BOT MEMORY "scheduled_lead_" + lead_id, lead_email
SET SCHEDULE DATEADD(NOW(), 7, "day"), "followup.bas"
' In followup.bas
PARAM lead_id AS string
lead_email = GET BOT MEMORY "scheduled_lead_" + lead_id
SEND TEMPLATE "followup", "email", lead_email, #{id: lead_id}
```
## Campaign Drip Sequence Example
```basic
' welcome-sequence.bas - File-level scheduler not used here
' This is triggered by form submission
PARAM email AS string
PARAM name AS string
DESCRIPTION "Start welcome email sequence"
' Day 0: Immediate welcome
WITH vars
.name = name
.date = TODAY()
END WITH
SEND TEMPLATE "welcome-1", "email", email, vars
' Store lead info for scheduled scripts
SET BOT MEMORY "welcome_" + email + "_name", name
' Schedule remaining emails
SET SCHEDULE DATEADD(NOW(), 2, "day"), "welcome-day-2.bas"
SET SCHEDULE DATEADD(NOW(), 5, "day"), "welcome-day-5.bas"
SET SCHEDULE DATEADD(NOW(), 7, "day"), "welcome-day-7.bas"
TALK "Welcome sequence started for " + email
```
## Daily Report Example
```basic
' daily-sales-report.bas
SET SCHEDULE "0 18 * * 1-5"
' This runs every weekday at 6 PM
today_sales = AGGREGATE "orders", "SUM", "total", "date = TODAY()"
order_count = AGGREGATE "orders", "COUNT", "id", "date = TODAY()"
avg_order = IIF(order_count > 0, today_sales / order_count, 0)
WITH report
.date = TODAY()
.total_sales = today_sales
.order_count = order_count
.average_order = ROUND(avg_order, 2)
END WITH
SEND TEMPLATE "daily-sales", "email", "sales@company.com", report
TALK "Daily report sent: $" + today_sales
```
## Canceling Scheduled Tasks
Scheduled tasks are stored in `system_automations` table. To cancel:
```basic
' Remove scheduled automation
DELETE "system_automations", "param = 'followup.bas' AND bot_id = '" + bot_id + "'"
```
## Best Practices
1. **Use descriptive script names**: `welcome-day-2.bas` not `email2.bas`
2. **Store context in BOT MEMORY**: Pass lead ID, not entire objects
3. **Check for unsubscribes**: Before sending scheduled emails
4. **Handle errors gracefully**: Scheduled scripts should not crash
5. **Log execution**: Track when scheduled scripts run
6. **Use appropriate times**: Consider timezone and recipient preferences
7. **Avoid overlapping schedules**: Don't schedule too many scripts at same time
## Database Schema
Scheduled tasks are stored in `system_automations`:
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Automation ID |
| `bot_id` | UUID | Bot owner |
| `kind` | INT | Trigger type (Scheduled = 1) |
| `schedule` | TEXT | Cron expression |
| `param` | TEXT | Script filename |
| `is_active` | BOOL | Active status |
| `last_triggered` | TIMESTAMP | Last execution time |
## Comparison
| Feature | File-Level | Runtime |
|---------|------------|---------|
| Location | Top of file | Anywhere in code |
| Purpose | Recurring execution | One-time scheduling |
| Timing | Cron-based recurring | Specific date/time |
| Script | Self (current file) | Other script |
| Use case | Reports, cleanup | Drip campaigns, reminders |

241
docs/TEMPLATE_VARIABLES.md Normal file
View file

@ -0,0 +1,241 @@
# Template Variables Reference
> Documentation for SEND TEMPLATE variables and built-in placeholders
## Overview
Templates support variable substitution using double curly braces `{{variable_name}}`. Variables are replaced at send time with values from the provided data object.
## Built-in Variables
These variables are automatically available in all templates:
| Variable | Description | Example Output |
|----------|-------------|----------------|
| `{{recipient}}` | Recipient email/phone | `john@example.com` |
| `{{to}}` | Alias for recipient | `john@example.com` |
| `{{date}}` | Current date (YYYY-MM-DD) | `2025-01-22` |
| `{{time}}` | Current time (HH:MM) | `14:30` |
| `{{datetime}}` | Date and time | `2025-01-22 14:30` |
| `{{year}}` | Current year | `2025` |
| `{{month}}` | Current month name | `January` |
## Custom Variables
Pass custom variables via the variables parameter:
```basic
WITH vars
.name = "John"
.company = "Acme Corp"
.product = "Pro Plan"
.discount = "20%"
END WITH
SEND TEMPLATE "welcome", "email", "john@example.com", vars
```
Template content:
```
Hello {{name}},
Welcome to {{company}}! You've signed up for {{product}}.
As a special offer, use code WELCOME for {{discount}} off your first purchase.
Best regards,
The Team
```
## Channel-Specific Templates
### Email Templates
Email templates support `Subject:` line extraction:
```
Subject: Welcome to {{company}}, {{name}}!
Hello {{name}},
Thank you for joining us...
```
### WhatsApp Templates
WhatsApp templates must be pre-approved by Meta. Use numbered placeholders:
```
Hello {{1}}, your order {{2}} has shipped. Track at {{3}}
```
Map variables:
```basic
WITH vars
.1 = customer_name
.2 = order_id
.3 = tracking_url
END WITH
SEND TEMPLATE "order-shipped", "whatsapp", phone, vars
```
### SMS Templates
Keep SMS templates under 160 characters for single segment:
```
Hi {{name}}, your code is {{code}}. Valid for 10 minutes.
```
## Template Examples
### Welcome Email
```
Subject: Welcome to {{company}}!
Hi {{name}},
Thanks for signing up on {{date}}. Here's what you can do next:
1. Complete your profile
2. Explore our features
3. Join our community
Questions? Reply to this email.
Best,
{{company}} Team
```
### Order Confirmation
```
Subject: Order #{{order_id}} Confirmed
Hi {{name}},
Your order has been confirmed!
Order: #{{order_id}}
Date: {{date}}
Total: {{total}}
Items:
{{items}}
Shipping to:
{{address}}
Track your order: {{tracking_url}}
```
### Lead Nurture
```
Subject: {{name}}, here's your exclusive resource
Hi {{name}},
As a {{company}} professional, we thought you'd find this helpful:
{{resource_title}}
{{resource_description}}
Download now: {{resource_url}}
Best,
{{sender_name}}
```
### Appointment Reminder
```
Subject: Reminder: {{appointment_type}} tomorrow
Hi {{name}},
This is a reminder of your upcoming appointment:
Date: {{appointment_date}}
Time: {{appointment_time}}
Location: {{location}}
Need to reschedule? Reply to this email or call {{phone}}.
See you soon!
```
## Creating Templates
### Via BASIC
```basic
CREATE TEMPLATE "welcome", "email", "Welcome {{name}}!", "Hello {{name}}, thank you for joining {{company}}!"
```
### Via Database
Templates are stored in `message_templates` table:
| Column | Type | Description |
|--------|------|-------------|
| `id` | UUID | Template ID |
| `bot_id` | UUID | Bot owner |
| `name` | TEXT | Template name |
| `channel` | TEXT | email/whatsapp/sms/telegram/push |
| `subject` | TEXT | Email subject (nullable) |
| `body` | TEXT | Template body |
| `variables` | JSONB | List of variable names |
| `is_active` | BOOL | Active status |
## Variable Extraction
Variables are automatically extracted from template body:
```basic
body = "Hello {{name}}, your order {{order_id}} is {{status}}."
' Extracted variables: ["name", "order_id", "status"]
```
Built-in variables (recipient, date, time, etc.) are excluded from extraction.
## Fallback Values
Use NVL for fallback values in your code:
```basic
WITH vars
.name = NVL(user_name, "Friend")
.company = NVL(user_company, "your organization")
END WITH
```
## Multi-Channel Example
Send same template to multiple channels:
```basic
WITH vars
.name = "John"
.message = "Your appointment is confirmed"
END WITH
' Send to all channels
SEND TEMPLATE "appointment-confirm", "email,sms,whatsapp", recipient, vars
' Or send separately with channel-specific content
SEND TEMPLATE "appointment-email", "email", email, vars
SEND TEMPLATE "appointment-sms", "sms", phone, vars
```
## Best Practices
1. **Keep variable names simple**: Use `name` not `customer_first_name`
2. **Provide fallbacks**: Always handle missing variables
3. **Test templates**: Verify all variables are populated
4. **Channel limits**: SMS 160 chars, WhatsApp requires approval
5. **Personalization**: Use `{{name}}` for better engagement
6. **Unsubscribe**: Include unsubscribe link in marketing emails

View file

@ -0,0 +1,192 @@
//! CONTAINS function for checking array membership
//!
//! BASIC Syntax:
//! result = CONTAINS(array, value)
//!
//! Returns TRUE if the value exists in the array, FALSE otherwise.
//!
//! Examples:
//! names = ["Alice", "Bob", "Charlie"]
//! IF CONTAINS(names, "Bob") THEN
//! TALK "Found Bob!"
//! END IF
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Array, Dynamic, Engine};
use std::sync::Arc;
/// Registers the CONTAINS function for array membership checking
pub fn contains_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// CONTAINS - uppercase version
engine.register_fn("CONTAINS", |arr: Array, value: Dynamic| -> bool {
array_contains(&arr, &value)
});
// contains - lowercase version
engine.register_fn("contains", |arr: Array, value: Dynamic| -> bool {
array_contains(&arr, &value)
});
// IN_ARRAY - alternative name (PHP style)
engine.register_fn("IN_ARRAY", |value: Dynamic, arr: Array| -> bool {
array_contains(&arr, &value)
});
engine.register_fn("in_array", |value: Dynamic, arr: Array| -> bool {
array_contains(&arr, &value)
});
// INCLUDES - JavaScript style
engine.register_fn("INCLUDES", |arr: Array, value: Dynamic| -> bool {
array_contains(&arr, &value)
});
engine.register_fn("includes", |arr: Array, value: Dynamic| -> bool {
array_contains(&arr, &value)
});
// HAS - short form
engine.register_fn("HAS", |arr: Array, value: Dynamic| -> bool {
array_contains(&arr, &value)
});
engine.register_fn("has", |arr: Array, value: Dynamic| -> bool {
array_contains(&arr, &value)
});
debug!("Registered CONTAINS keyword");
}
/// Helper function to check if an array contains a value
fn array_contains(arr: &Array, value: &Dynamic) -> bool {
let search_str = value.to_string();
for item in arr {
// Try exact type match first
if items_equal(item, value) {
return true;
}
// Fall back to string comparison
if item.to_string() == search_str {
return true;
}
}
false
}
/// Helper function to compare two Dynamic values
fn items_equal(a: &Dynamic, b: &Dynamic) -> bool {
// Both integers
if a.is_int() && b.is_int() {
return a.as_int().unwrap_or(0) == b.as_int().unwrap_or(1);
}
// Both floats
if a.is_float() && b.is_float() {
let af = a.as_float().unwrap_or(0.0);
let bf = b.as_float().unwrap_or(1.0);
return (af - bf).abs() < f64::EPSILON;
}
// Int and float comparison
if a.is_int() && b.is_float() {
let af = a.as_int().unwrap_or(0) as f64;
let bf = b.as_float().unwrap_or(1.0);
return (af - bf).abs() < f64::EPSILON;
}
if a.is_float() && b.is_int() {
let af = a.as_float().unwrap_or(0.0);
let bf = b.as_int().unwrap_or(1) as f64;
return (af - bf).abs() < f64::EPSILON;
}
// Both booleans
if a.is_bool() && b.is_bool() {
return a.as_bool().unwrap_or(false) == b.as_bool().unwrap_or(true);
}
// Both strings
if a.is_string() && b.is_string() {
return a.clone().into_string().unwrap_or_default()
== b.clone().into_string().unwrap_or_default();
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_contains_string() {
let mut arr = Array::new();
arr.push(Dynamic::from("Alice"));
arr.push(Dynamic::from("Bob"));
arr.push(Dynamic::from("Charlie"));
assert!(array_contains(&arr, &Dynamic::from("Bob")));
assert!(!array_contains(&arr, &Dynamic::from("David")));
}
#[test]
fn test_contains_integer() {
let mut arr = Array::new();
arr.push(Dynamic::from(1_i64));
arr.push(Dynamic::from(2_i64));
arr.push(Dynamic::from(3_i64));
assert!(array_contains(&arr, &Dynamic::from(2_i64)));
assert!(!array_contains(&arr, &Dynamic::from(5_i64)));
}
#[test]
fn test_contains_float() {
let mut arr = Array::new();
arr.push(Dynamic::from(1.5_f64));
arr.push(Dynamic::from(2.5_f64));
arr.push(Dynamic::from(3.5_f64));
assert!(array_contains(&arr, &Dynamic::from(2.5_f64)));
assert!(!array_contains(&arr, &Dynamic::from(4.5_f64)));
}
#[test]
fn test_contains_bool() {
let mut arr = Array::new();
arr.push(Dynamic::from(true));
arr.push(Dynamic::from(false));
assert!(array_contains(&arr, &Dynamic::from(true)));
assert!(array_contains(&arr, &Dynamic::from(false)));
}
#[test]
fn test_contains_empty_array() {
let arr = Array::new();
assert!(!array_contains(&arr, &Dynamic::from("anything")));
}
#[test]
fn test_items_equal_integers() {
assert!(items_equal(&Dynamic::from(5_i64), &Dynamic::from(5_i64)));
assert!(!items_equal(&Dynamic::from(5_i64), &Dynamic::from(6_i64)));
}
#[test]
fn test_items_equal_strings() {
assert!(items_equal(
&Dynamic::from("hello"),
&Dynamic::from("hello")
));
assert!(!items_equal(
&Dynamic::from("hello"),
&Dynamic::from("world")
));
}
}

View file

@ -0,0 +1,273 @@
pub mod contains;
pub mod push_pop;
pub mod slice;
pub mod sort;
pub mod unique;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Array, Dynamic, Engine};
use std::sync::Arc;
pub fn register_array_functions(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
sort::sort_keyword(&state, user.clone(), engine);
unique::unique_keyword(&state, user.clone(), engine);
contains::contains_keyword(&state, user.clone(), engine);
push_pop::push_keyword(&state, user.clone(), engine);
push_pop::pop_keyword(&state, user.clone(), engine);
push_pop::shift_keyword(&state, user.clone(), engine);
push_pop::unshift_keyword(&state, user.clone(), engine);
slice::slice_keyword(&state, user.clone(), engine);
register_utility_functions(engine);
debug!("Registered all array functions");
}
fn register_utility_functions(engine: &mut Engine) {
// UBOUND - upper bound (length - 1)
engine.register_fn("UBOUND", |arr: Array| -> i64 {
if arr.is_empty() {
-1
} else {
(arr.len() - 1) as i64
}
});
engine.register_fn("ubound", |arr: Array| -> i64 {
if arr.is_empty() {
-1
} else {
(arr.len() - 1) as i64
}
});
// LBOUND - lower bound (always 0)
engine.register_fn("LBOUND", |_arr: Array| -> i64 { 0 });
engine.register_fn("lbound", |_arr: Array| -> i64 { 0 });
// COUNT - array length
engine.register_fn("COUNT", |arr: Array| -> i64 { arr.len() as i64 });
engine.register_fn("count", |arr: Array| -> i64 { arr.len() as i64 });
// LEN for arrays (alias for COUNT)
engine.register_fn("LEN", |arr: Array| -> i64 { arr.len() as i64 });
engine.register_fn("len", |arr: Array| -> i64 { arr.len() as i64 });
// SIZE alias
engine.register_fn("SIZE", |arr: Array| -> i64 { arr.len() as i64 });
engine.register_fn("size", |arr: Array| -> i64 { arr.len() as i64 });
// REVERSE
engine.register_fn("REVERSE", |arr: Array| -> Array {
let mut reversed = arr.clone();
reversed.reverse();
reversed
});
engine.register_fn("reverse", |arr: Array| -> Array {
let mut reversed = arr.clone();
reversed.reverse();
reversed
});
// JOIN - array to string
engine.register_fn("JOIN", |arr: Array, separator: &str| -> String {
arr.iter()
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join(separator)
});
engine.register_fn("join", |arr: Array, separator: &str| -> String {
arr.iter()
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join(separator)
});
// JOIN with default separator (comma)
engine.register_fn("JOIN", |arr: Array| -> String {
arr.iter()
.map(|item| item.to_string())
.collect::<Vec<_>>()
.join(",")
});
// SPLIT - string to array
engine.register_fn("SPLIT", |s: &str, delimiter: &str| -> Array {
s.split(delimiter)
.map(|part| Dynamic::from(part.to_string()))
.collect()
});
engine.register_fn("split", |s: &str, delimiter: &str| -> Array {
s.split(delimiter)
.map(|part| Dynamic::from(part.to_string()))
.collect()
});
// RANGE - create array of numbers
engine.register_fn("RANGE", |start: i64, end: i64| -> Array {
(start..=end).map(Dynamic::from).collect()
});
engine.register_fn("range", |start: i64, end: i64| -> Array {
(start..=end).map(Dynamic::from).collect()
});
// RANGE with step
engine.register_fn("RANGE", |start: i64, end: i64, step: i64| -> Array {
if step == 0 {
return Array::new();
}
let mut result = Array::new();
let mut current = start;
if step > 0 {
while current <= end {
result.push(Dynamic::from(current));
current += step;
}
} else {
while current >= end {
result.push(Dynamic::from(current));
current += step;
}
}
result
});
// INDEX_OF
engine.register_fn("INDEX_OF", |arr: Array, value: Dynamic| -> i64 {
let search = value.to_string();
arr.iter()
.position(|item| item.to_string() == search)
.map(|i| i as i64)
.unwrap_or(-1)
});
engine.register_fn("index_of", |arr: Array, value: Dynamic| -> i64 {
let search = value.to_string();
arr.iter()
.position(|item| item.to_string() == search)
.map(|i| i as i64)
.unwrap_or(-1)
});
// LAST_INDEX_OF
engine.register_fn("LAST_INDEX_OF", |arr: Array, value: Dynamic| -> i64 {
let search = value.to_string();
arr.iter()
.rposition(|item| item.to_string() == search)
.map(|i| i as i64)
.unwrap_or(-1)
});
// CONCAT - combine arrays
engine.register_fn("CONCAT", |arr1: Array, arr2: Array| -> Array {
let mut result = arr1.clone();
result.extend(arr2);
result
});
engine.register_fn("concat", |arr1: Array, arr2: Array| -> Array {
let mut result = arr1.clone();
result.extend(arr2);
result
});
// FIRST_ELEM / FIRST
engine.register_fn("FIRST_ELEM", |arr: Array| -> Dynamic {
arr.first().cloned().unwrap_or(Dynamic::UNIT)
});
engine.register_fn("FIRST", |arr: Array| -> Dynamic {
arr.first().cloned().unwrap_or(Dynamic::UNIT)
});
engine.register_fn("first", |arr: Array| -> Dynamic {
arr.first().cloned().unwrap_or(Dynamic::UNIT)
});
// LAST_ELEM / LAST
engine.register_fn("LAST_ELEM", |arr: Array| -> Dynamic {
arr.last().cloned().unwrap_or(Dynamic::UNIT)
});
engine.register_fn("LAST", |arr: Array| -> Dynamic {
arr.last().cloned().unwrap_or(Dynamic::UNIT)
});
engine.register_fn("last", |arr: Array| -> Dynamic {
arr.last().cloned().unwrap_or(Dynamic::UNIT)
});
// FLATTEN - flatten nested arrays (one level)
engine.register_fn("FLATTEN", |arr: Array| -> Array {
let mut result = Array::new();
for item in arr {
if item.is_array() {
if let Ok(inner) = item.into_array() {
result.extend(inner);
}
} else {
result.push(item);
}
}
result
});
engine.register_fn("flatten", |arr: Array| -> Array {
let mut result = Array::new();
for item in arr {
if item.is_array() {
if let Ok(inner) = item.into_array() {
result.extend(inner);
}
} else {
result.push(item);
}
}
result
});
// EMPTY - create empty array
engine.register_fn("EMPTY_ARRAY", || -> Array { Array::new() });
// FILL - create array filled with value
engine.register_fn("FILL", |value: Dynamic, count: i64| -> Array {
(0..count).map(|_| value.clone()).collect()
});
engine.register_fn("fill", |value: Dynamic, count: i64| -> Array {
(0..count).map(|_| value.clone()).collect()
});
debug!("Registered array utility functions");
}
#[cfg(test)]
mod tests {
use rhai::Dynamic;
#[test]
fn test_ubound() {
let arr: Vec<Dynamic> = vec![Dynamic::from(1), Dynamic::from(2), Dynamic::from(3)];
assert_eq!(arr.len() - 1, 2);
}
#[test]
fn test_join() {
let arr = vec!["a", "b", "c"];
let result = arr.join("-");
assert_eq!(result, "a-b-c");
}
#[test]
fn test_split() {
let s = "a,b,c";
let parts: Vec<&str> = s.split(',').collect();
assert_eq!(parts.len(), 3);
}
#[test]
fn test_range() {
let range: Vec<i64> = (1..=5).collect();
assert_eq!(range, vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_flatten() {
// Test flattening logic
let nested = vec![vec![1, 2], vec![3, 4]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();
assert_eq!(flat, vec![1, 2, 3, 4]);
}
}

View file

@ -0,0 +1,167 @@
//! PUSH and POP array manipulation functions
//!
//! PUSH - Add element(s) to the end of an array
//! POP - Remove and return the last element from an array
//! SHIFT - Remove and return the first element from an array
//! UNSHIFT - Add element(s) to the beginning of an array
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Array, Dynamic, Engine};
use std::sync::Arc;
/// PUSH - Add an element to the end of an array
/// Returns the new array with the element added
pub fn push_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// PUSH single element
engine.register_fn("PUSH", |mut arr: Array, value: Dynamic| -> Array {
arr.push(value);
arr
});
engine.register_fn("push", |mut arr: Array, value: Dynamic| -> Array {
arr.push(value);
arr
});
// ARRAY_PUSH alias
engine.register_fn("ARRAY_PUSH", |mut arr: Array, value: Dynamic| -> Array {
arr.push(value);
arr
});
// APPEND alias
engine.register_fn("APPEND", |mut arr: Array, value: Dynamic| -> Array {
arr.push(value);
arr
});
engine.register_fn("append", |mut arr: Array, value: Dynamic| -> Array {
arr.push(value);
arr
});
debug!("Registered PUSH keyword");
}
/// POP - Remove and return the last element from an array
/// Returns the removed element (or unit if array is empty)
pub fn pop_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// POP - returns the popped element
engine.register_fn("POP", |mut arr: Array| -> Dynamic {
arr.pop().unwrap_or(Dynamic::UNIT)
});
engine.register_fn("pop", |mut arr: Array| -> Dynamic {
arr.pop().unwrap_or(Dynamic::UNIT)
});
// ARRAY_POP alias
engine.register_fn("ARRAY_POP", |mut arr: Array| -> Dynamic {
arr.pop().unwrap_or(Dynamic::UNIT)
});
debug!("Registered POP keyword");
}
/// SHIFT - Remove and return the first element from an array
pub fn shift_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("SHIFT", |mut arr: Array| -> Dynamic {
if arr.is_empty() {
Dynamic::UNIT
} else {
arr.remove(0)
}
});
engine.register_fn("shift", |mut arr: Array| -> Dynamic {
if arr.is_empty() {
Dynamic::UNIT
} else {
arr.remove(0)
}
});
// ARRAY_SHIFT alias
engine.register_fn("ARRAY_SHIFT", |mut arr: Array| -> Dynamic {
if arr.is_empty() {
Dynamic::UNIT
} else {
arr.remove(0)
}
});
debug!("Registered SHIFT keyword");
}
/// UNSHIFT - Add element(s) to the beginning of an array
pub fn unshift_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("UNSHIFT", |mut arr: Array, value: Dynamic| -> Array {
arr.insert(0, value);
arr
});
engine.register_fn("unshift", |mut arr: Array, value: Dynamic| -> Array {
arr.insert(0, value);
arr
});
// PREPEND alias
engine.register_fn("PREPEND", |mut arr: Array, value: Dynamic| -> Array {
arr.insert(0, value);
arr
});
engine.register_fn("prepend", |mut arr: Array, value: Dynamic| -> Array {
arr.insert(0, value);
arr
});
debug!("Registered UNSHIFT keyword");
}
#[cfg(test)]
mod tests {
use rhai::{Array, Dynamic};
#[test]
fn test_push() {
let mut arr: Array = vec![Dynamic::from(1), Dynamic::from(2)];
arr.push(Dynamic::from(3));
assert_eq!(arr.len(), 3);
assert_eq!(arr[2].as_int().unwrap(), 3);
}
#[test]
fn test_pop() {
let mut arr: Array = vec![Dynamic::from(1), Dynamic::from(2), Dynamic::from(3)];
let popped = arr.pop().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(popped.as_int().unwrap(), 3);
}
#[test]
fn test_pop_empty() {
let mut arr: Array = vec![];
let popped = arr.pop();
assert!(popped.is_none());
}
#[test]
fn test_shift() {
let mut arr: Array = vec![Dynamic::from(1), Dynamic::from(2), Dynamic::from(3)];
let shifted = arr.remove(0);
assert_eq!(arr.len(), 2);
assert_eq!(shifted.as_int().unwrap(), 1);
assert_eq!(arr[0].as_int().unwrap(), 2);
}
#[test]
fn test_unshift() {
let mut arr: Array = vec![Dynamic::from(2), Dynamic::from(3)];
arr.insert(0, Dynamic::from(1));
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_int().unwrap(), 1);
}
}

View file

@ -0,0 +1,204 @@
//! SLICE function for extracting portions of arrays
//!
//! BASIC Syntax:
//! result = SLICE(array, start) ' From start to end
//! result = SLICE(array, start, end) ' From start to end (exclusive)
//!
//! Examples:
//! arr = [1, 2, 3, 4, 5]
//! part = SLICE(arr, 2) ' Returns [3, 4, 5] (0-based index)
//! part = SLICE(arr, 1, 3) ' Returns [2, 3] (indices 1 and 2)
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Array, Dynamic, Engine};
use std::sync::Arc;
/// Register the SLICE keyword for array slicing
pub fn slice_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// SLICE with start only (from start to end)
engine.register_fn("SLICE", |arr: Array, start: i64| -> Array {
slice_array(&arr, start, None)
});
engine.register_fn("slice", |arr: Array, start: i64| -> Array {
slice_array(&arr, start, None)
});
// SLICE with start and end
engine.register_fn("SLICE", |arr: Array, start: i64, end: i64| -> Array {
slice_array(&arr, start, Some(end))
});
engine.register_fn("slice", |arr: Array, start: i64, end: i64| -> Array {
slice_array(&arr, start, Some(end))
});
// SUBARRAY alias
engine.register_fn("SUBARRAY", |arr: Array, start: i64, end: i64| -> Array {
slice_array(&arr, start, Some(end))
});
// MID for arrays (similar to MID$ for strings)
engine.register_fn(
"MID_ARRAY",
|arr: Array, start: i64, length: i64| -> Array {
let end = start + length;
slice_array(&arr, start, Some(end))
},
);
// TAKE - take first n elements
engine.register_fn("TAKE", |arr: Array, count: i64| -> Array {
slice_array(&arr, 0, Some(count))
});
engine.register_fn("take", |arr: Array, count: i64| -> Array {
slice_array(&arr, 0, Some(count))
});
// DROP - drop first n elements
engine.register_fn("DROP", |arr: Array, count: i64| -> Array {
slice_array(&arr, count, None)
});
engine.register_fn("drop", |arr: Array, count: i64| -> Array {
slice_array(&arr, count, None)
});
// HEAD - first element
engine.register_fn("HEAD", |arr: Array| -> Dynamic {
arr.first().cloned().unwrap_or(Dynamic::UNIT)
});
engine.register_fn("head", |arr: Array| -> Dynamic {
arr.first().cloned().unwrap_or(Dynamic::UNIT)
});
// TAIL - all elements except first
engine.register_fn("TAIL", |arr: Array| -> Array {
if arr.len() <= 1 {
Array::new()
} else {
arr[1..].to_vec()
}
});
engine.register_fn("tail", |arr: Array| -> Array {
if arr.len() <= 1 {
Array::new()
} else {
arr[1..].to_vec()
}
});
// INIT - all elements except last
engine.register_fn("INIT", |arr: Array| -> Array {
if arr.is_empty() {
Array::new()
} else {
arr[..arr.len() - 1].to_vec()
}
});
// LAST_ELEM - last element
engine.register_fn("LAST", |arr: Array| -> Dynamic {
arr.last().cloned().unwrap_or(Dynamic::UNIT)
});
debug!("Registered SLICE keyword");
}
/// Helper function to slice an array
fn slice_array(arr: &Array, start: i64, end: Option<i64>) -> Array {
let len = arr.len() as i64;
// Handle negative indices (count from end)
let start_idx = if start < 0 {
(len + start).max(0) as usize
} else {
(start as usize).min(arr.len())
};
let end_idx = match end {
Some(e) => {
if e < 0 {
(len + e).max(0) as usize
} else {
(e as usize).min(arr.len())
}
}
None => arr.len(),
};
if start_idx >= end_idx || start_idx >= arr.len() {
return Array::new();
}
arr[start_idx..end_idx].to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_array() -> Array {
vec![
Dynamic::from(1),
Dynamic::from(2),
Dynamic::from(3),
Dynamic::from(4),
Dynamic::from(5),
]
}
#[test]
fn test_slice_from_start() {
let arr = make_test_array();
let result = slice_array(&arr, 2, None);
assert_eq!(result.len(), 3);
assert_eq!(result[0].as_int().unwrap(), 3);
}
#[test]
fn test_slice_with_end() {
let arr = make_test_array();
let result = slice_array(&arr, 1, Some(3));
assert_eq!(result.len(), 2);
assert_eq!(result[0].as_int().unwrap(), 2);
assert_eq!(result[1].as_int().unwrap(), 3);
}
#[test]
fn test_slice_negative_start() {
let arr = make_test_array();
let result = slice_array(&arr, -2, None);
assert_eq!(result.len(), 2);
assert_eq!(result[0].as_int().unwrap(), 4);
assert_eq!(result[1].as_int().unwrap(), 5);
}
#[test]
fn test_slice_negative_end() {
let arr = make_test_array();
let result = slice_array(&arr, 0, Some(-2));
assert_eq!(result.len(), 3);
assert_eq!(result[0].as_int().unwrap(), 1);
assert_eq!(result[2].as_int().unwrap(), 3);
}
#[test]
fn test_slice_out_of_bounds() {
let arr = make_test_array();
let result = slice_array(&arr, 10, None);
assert!(result.is_empty());
}
#[test]
fn test_slice_empty_range() {
let arr = make_test_array();
let result = slice_array(&arr, 3, Some(2));
assert!(result.is_empty());
}
}

View file

@ -0,0 +1,148 @@
//! SORT function for array sorting
//!
//! Provides sorting capabilities for arrays in BASIC scripts.
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Array, Dynamic, Engine};
use std::sync::Arc;
/// SORT - Sort an array in ascending order
/// Syntax: sorted_array = SORT(array)
/// sorted_array = SORT(array, "DESC")
pub fn sort_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// SORT ascending (default)
engine.register_fn("SORT", |arr: Array| -> Array { sort_array(arr, false) });
engine.register_fn("sort", |arr: Array| -> Array { sort_array(arr, false) });
// SORT with direction parameter
engine.register_fn("SORT", |arr: Array, direction: &str| -> Array {
let desc =
direction.eq_ignore_ascii_case("DESC") || direction.eq_ignore_ascii_case("DESCENDING");
sort_array(arr, desc)
});
engine.register_fn("sort", |arr: Array, direction: &str| -> Array {
let desc =
direction.eq_ignore_ascii_case("DESC") || direction.eq_ignore_ascii_case("DESCENDING");
sort_array(arr, desc)
});
// SORT_ASC - explicit ascending sort
engine.register_fn("SORT_ASC", |arr: Array| -> Array { sort_array(arr, false) });
engine.register_fn("sort_asc", |arr: Array| -> Array { sort_array(arr, false) });
// SORT_DESC - explicit descending sort
engine.register_fn("SORT_DESC", |arr: Array| -> Array { sort_array(arr, true) });
engine.register_fn("sort_desc", |arr: Array| -> Array { sort_array(arr, true) });
debug!("Registered SORT keyword");
}
/// Helper function to sort an array
fn sort_array(arr: Array, descending: bool) -> Array {
let mut sorted = arr.clone();
sorted.sort_by(|a, b| {
let cmp = compare_dynamic(a, b);
if descending {
cmp.reverse()
} else {
cmp
}
});
sorted
}
/// Compare two Dynamic values
fn compare_dynamic(a: &Dynamic, b: &Dynamic) -> std::cmp::Ordering {
// Try numeric comparison first
if let (Some(a_num), Some(b_num)) = (to_f64(a), to_f64(b)) {
return a_num
.partial_cmp(&b_num)
.unwrap_or(std::cmp::Ordering::Equal);
}
// Fall back to string comparison
a.to_string().cmp(&b.to_string())
}
/// Try to convert a Dynamic value to f64
fn to_f64(value: &Dynamic) -> Option<f64> {
if value.is_int() {
value.as_int().ok().map(|i| i as f64)
} else if value.is_float() {
value.as_float().ok()
} else if value.is_string() {
value
.clone()
.into_string()
.ok()
.and_then(|s| s.parse().ok())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sort_integers() {
let arr: Array = vec![
Dynamic::from(3),
Dynamic::from(1),
Dynamic::from(4),
Dynamic::from(1),
Dynamic::from(5),
];
let sorted = sort_array(arr, false);
assert_eq!(sorted[0].as_int().unwrap(), 1);
assert_eq!(sorted[1].as_int().unwrap(), 1);
assert_eq!(sorted[2].as_int().unwrap(), 3);
assert_eq!(sorted[3].as_int().unwrap(), 4);
assert_eq!(sorted[4].as_int().unwrap(), 5);
}
#[test]
fn test_sort_strings() {
let arr: Array = vec![
Dynamic::from("banana"),
Dynamic::from("apple"),
Dynamic::from("cherry"),
];
let sorted = sort_array(arr, false);
assert_eq!(sorted[0].clone().into_string().unwrap(), "apple");
assert_eq!(sorted[1].clone().into_string().unwrap(), "banana");
assert_eq!(sorted[2].clone().into_string().unwrap(), "cherry");
}
#[test]
fn test_sort_descending() {
let arr: Array = vec![Dynamic::from(1), Dynamic::from(3), Dynamic::from(2)];
let sorted = sort_array(arr, true);
assert_eq!(sorted[0].as_int().unwrap(), 3);
assert_eq!(sorted[1].as_int().unwrap(), 2);
assert_eq!(sorted[2].as_int().unwrap(), 1);
}
#[test]
fn test_compare_dynamic_numbers() {
let a = Dynamic::from(5);
let b = Dynamic::from(3);
assert_eq!(compare_dynamic(&a, &b), std::cmp::Ordering::Greater);
}
#[test]
fn test_compare_dynamic_strings() {
let a = Dynamic::from("apple");
let b = Dynamic::from("banana");
assert_eq!(compare_dynamic(&a, &b), std::cmp::Ordering::Less);
}
}

View file

@ -0,0 +1,142 @@
//! UNIQUE - Remove duplicate values from an array
//!
//! BASIC Syntax:
//! result = UNIQUE(array)
//!
//! Examples:
//! numbers = [1, 2, 2, 3, 3, 3, 4]
//! unique_numbers = UNIQUE(numbers) ' Returns [1, 2, 3, 4]
//!
//! names = ["Alice", "Bob", "Alice", "Charlie"]
//! unique_names = UNIQUE(names) ' Returns ["Alice", "Bob", "Charlie"]
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Array, Dynamic, Engine};
use std::collections::HashSet;
use std::sync::Arc;
/// Register the UNIQUE function for removing duplicate values from arrays
pub fn unique_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// UNIQUE - uppercase version
engine.register_fn("UNIQUE", |arr: Array| -> Array { unique_array(arr) });
// unique - lowercase version
engine.register_fn("unique", |arr: Array| -> Array { unique_array(arr) });
// DISTINCT - SQL-style alias
engine.register_fn("DISTINCT", |arr: Array| -> Array { unique_array(arr) });
engine.register_fn("distinct", |arr: Array| -> Array { unique_array(arr) });
debug!("Registered UNIQUE keyword");
}
/// Helper function to remove duplicates from an array
/// Preserves the order of first occurrence
fn unique_array(arr: Array) -> Array {
let mut seen: HashSet<String> = HashSet::new();
let mut result = Array::new();
for item in arr {
// Use string representation for comparison
// This handles most common types (strings, numbers, bools)
let key = item.to_string();
if !seen.contains(&key) {
seen.insert(key);
result.push(item);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unique_integers() {
let mut arr = Array::new();
arr.push(Dynamic::from(1_i64));
arr.push(Dynamic::from(2_i64));
arr.push(Dynamic::from(2_i64));
arr.push(Dynamic::from(3_i64));
arr.push(Dynamic::from(3_i64));
arr.push(Dynamic::from(3_i64));
arr.push(Dynamic::from(4_i64));
let result = unique_array(arr);
assert_eq!(result.len(), 4);
}
#[test]
fn test_unique_strings() {
let mut arr = Array::new();
arr.push(Dynamic::from("Alice"));
arr.push(Dynamic::from("Bob"));
arr.push(Dynamic::from("Alice"));
arr.push(Dynamic::from("Charlie"));
let result = unique_array(arr);
assert_eq!(result.len(), 3);
}
#[test]
fn test_unique_preserves_order() {
let mut arr = Array::new();
arr.push(Dynamic::from("C"));
arr.push(Dynamic::from("A"));
arr.push(Dynamic::from("B"));
arr.push(Dynamic::from("A"));
arr.push(Dynamic::from("C"));
let result = unique_array(arr);
assert_eq!(result.len(), 3);
assert_eq!(result[0].to_string(), "C");
assert_eq!(result[1].to_string(), "A");
assert_eq!(result[2].to_string(), "B");
}
#[test]
fn test_unique_empty_array() {
let arr = Array::new();
let result = unique_array(arr);
assert!(result.is_empty());
}
#[test]
fn test_unique_single_element() {
let mut arr = Array::new();
arr.push(Dynamic::from(42_i64));
let result = unique_array(arr);
assert_eq!(result.len(), 1);
}
#[test]
fn test_unique_all_same() {
let mut arr = Array::new();
arr.push(Dynamic::from(1_i64));
arr.push(Dynamic::from(1_i64));
arr.push(Dynamic::from(1_i64));
let result = unique_array(arr);
assert_eq!(result.len(), 1);
}
#[test]
fn test_unique_mixed_types() {
let mut arr = Array::new();
arr.push(Dynamic::from(1_i64));
arr.push(Dynamic::from("1"));
arr.push(Dynamic::from(1_i64));
let result = unique_array(arr);
// "1" (int) and "1" (string) may have same string representation
// so behavior depends on Dynamic::to_string() implementation
assert!(result.len() >= 1 && result.len() <= 2);
}
}

View file

@ -0,0 +1,136 @@
//! Core BASIC Functions - Wrapper module
//!
//! This module serves as a central registration point for all core BASIC functions:
//! - Math functions (ABS, ROUND, INT, MAX, MIN, MOD, RANDOM, etc.)
//! - Date/Time functions (NOW, TODAY, YEAR, MONTH, DAY, etc.)
//! - Validation functions (VAL, STR, ISNULL, ISEMPTY, TYPEOF, etc.)
//! - Array functions (SORT, UNIQUE, CONTAINS, PUSH, POP, etc.)
//! - Error handling functions (THROW, ERROR, IS_ERROR, ASSERT, etc.)
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::Engine;
use std::sync::Arc;
use super::arrays::register_array_functions;
use super::datetime::register_datetime_functions;
use super::errors::register_error_functions;
use super::math::register_math_functions;
use super::validation::register_validation_functions;
/// Register all core BASIC functions
///
/// This function registers all the standard BASIC functions that are commonly
/// expected in any BASIC implementation:
///
/// ## Math Functions
/// - `ABS(n)` - Absolute value
/// - `ROUND(n)`, `ROUND(n, decimals)` - Round to nearest integer or decimal places
/// - `INT(n)`, `FIX(n)` - Truncate to integer
/// - `FLOOR(n)`, `CEIL(n)` - Floor and ceiling
/// - `MAX(a, b)`, `MIN(a, b)` - Maximum and minimum
/// - `MOD(a, b)` - Modulo operation
/// - `RANDOM()`, `RND()` - Random number generation
/// - `SGN(n)` - Sign of number (-1, 0, 1)
/// - `SQRT(n)`, `SQR(n)` - Square root
/// - `POW(base, exp)` - Power/exponentiation
/// - `LOG(n)`, `LOG10(n)` - Natural and base-10 logarithm
/// - `EXP(n)` - e raised to power n
/// - `SIN(n)`, `COS(n)`, `TAN(n)` - Trigonometric functions
/// - `ASIN(n)`, `ACOS(n)`, `ATAN(n)` - Inverse trigonometric
/// - `PI()` - Pi constant
/// - `SUM(array)`, `AVG(array)` - Aggregation functions
///
/// ## Date/Time Functions
/// - `NOW()` - Current date and time
/// - `TODAY()` - Current date only
/// - `TIME()` - Current time only
/// - `TIMESTAMP()` - Unix timestamp
/// - `YEAR(date)`, `MONTH(date)`, `DAY(date)` - Extract date parts
/// - `HOUR(time)`, `MINUTE(time)`, `SECOND(time)` - Extract time parts
/// - `WEEKDAY(date)` - Day of week (1-7)
/// - `DATEADD(date, amount, unit)` - Add to date
/// - `DATEDIFF(date1, date2, unit)` - Difference between dates
/// - `FORMAT_DATE(date, format)` - Format date as string
/// - `ISDATE(value)` - Check if value is a valid date
///
/// ## Validation Functions
/// - `VAL(string)` - Convert string to number
/// - `STR(number)` - Convert number to string
/// - `CINT(value)` - Convert to integer
/// - `CDBL(value)` - Convert to double
/// - `ISNULL(value)` - Check if null/unit
/// - `ISEMPTY(value)` - Check if empty (string, array, map)
/// - `TYPEOF(value)` - Get type name as string
/// - `ISARRAY(value)` - Check if array
/// - `ISNUMBER(value)` - Check if number
/// - `ISSTRING(value)` - Check if string
/// - `ISBOOL(value)` - Check if boolean
/// - `NVL(value, default)` - Null coalesce
/// - `IIF(condition, true_val, false_val)` - Immediate if
///
/// ## Array Functions
/// - `UBOUND(array)` - Upper bound (length - 1)
/// - `LBOUND(array)` - Lower bound (always 0)
/// - `COUNT(array)` - Array length
/// - `SORT(array)`, `SORT(array, "DESC")` - Sort array
/// - `UNIQUE(array)` - Remove duplicates
/// - `CONTAINS(array, value)` - Check membership
/// - `INDEX_OF(array, value)` - Find index of value
/// - `PUSH(array, value)` - Add to end
/// - `POP(array)` - Remove from end
/// - `SHIFT(array)` - Remove from beginning
/// - `UNSHIFT(array, value)` - Add to beginning
/// - `REVERSE(array)` - Reverse array
/// - `SLICE(array, start, end)` - Extract portion
/// - `JOIN(array, separator)` - Join to string
/// - `SPLIT(string, delimiter)` - Split to array
/// - `CONCAT(array1, array2)` - Combine arrays
/// - `RANGE(start, end)` - Create number range
///
/// ## Error Handling Functions
/// - `THROW(message)`, `RAISE(message)` - Throw error
/// - `ERROR(message)` - Create error object
/// - `IS_ERROR(value)` - Check if error object
/// - `GET_ERROR_MESSAGE(error)` - Get error message
/// - `ASSERT(condition, message)` - Assert condition
/// - `LOG_ERROR(message)` - Log error
/// - `LOG_WARN(message)` - Log warning
/// - `LOG_INFO(message)` - Log info
/// - `LOG_DEBUG(message)` - Log debug
pub fn register_core_functions(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
debug!("Registering core BASIC functions...");
// Register math functions (ABS, ROUND, INT, MAX, MIN, MOD, RANDOM, etc.)
register_math_functions(state.clone(), user.clone(), engine);
debug!(" ✓ Math functions registered");
// Register date/time functions (NOW, TODAY, YEAR, MONTH, DAY, etc.)
register_datetime_functions(state.clone(), user.clone(), engine);
debug!(" ✓ Date/Time functions registered");
// Register validation functions (VAL, STR, ISNULL, ISEMPTY, TYPEOF, etc.)
register_validation_functions(state.clone(), user.clone(), engine);
debug!(" ✓ Validation functions registered");
// Register array functions (SORT, UNIQUE, CONTAINS, PUSH, POP, etc.)
register_array_functions(state.clone(), user.clone(), engine);
debug!(" ✓ Array functions registered");
// Register error handling functions (THROW, ERROR, IS_ERROR, ASSERT, etc.)
register_error_functions(state, user, engine);
debug!(" ✓ Error handling functions registered");
debug!("All core BASIC functions registered successfully");
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_structure() {
// This test verifies the module compiles correctly
// Actual function tests are in their respective submodules
assert!(true);
}
}

View file

@ -0,0 +1,17 @@
pub mod score_lead;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::Engine;
use std::sync::Arc;
pub fn register_crm_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
score_lead::score_lead_keyword(state.clone(), user.clone(), engine);
score_lead::get_lead_score_keyword(state.clone(), user.clone(), engine);
score_lead::qualify_lead_keyword(state.clone(), user.clone(), engine);
score_lead::update_lead_score_keyword(state.clone(), user.clone(), engine);
score_lead::ai_score_lead_keyword(state, user, engine);
debug!("Registered all CRM keywords");
}

View file

@ -0,0 +1,519 @@
//! Lead Scoring Functions for CRM Integration
//!
//! Provides BASIC keywords for lead scoring and qualification:
//! - SCORE_LEAD - Calculate lead score based on criteria
//! - GET_LEAD_SCORE - Retrieve stored lead score
//! - QUALIFY_LEAD - Check if lead meets qualification threshold
//! - UPDATE_LEAD_SCORE - Manually adjust lead score
//! - AI_SCORE_LEAD - LLM-enhanced lead scoring
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{debug, trace};
use rhai::{Dynamic, Engine, Map};
use std::sync::Arc;
/// SCORE_LEAD - Calculate lead score based on provided criteria
///
/// BASIC Syntax:
/// score = SCORE_LEAD(lead_data)
/// score = SCORE_LEAD(lead_data, scoring_rules)
///
/// Examples:
/// lead = #{"email": "john@company.com", "job_title": "CTO", "company_size": 500}
/// score = SCORE_LEAD(lead)
pub fn score_lead_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
let user_clone = user.clone();
// SCORE_LEAD with lead data only (uses default scoring)
engine.register_fn("SCORE_LEAD", move |lead_data: Map| -> i64 {
trace!(
"SCORE_LEAD called for user {} with data: {:?}",
user_clone.user_id,
lead_data
);
calculate_lead_score(&lead_data, None)
});
let state_clone2 = state.clone();
let user_clone2 = user.clone();
// score_lead lowercase version
engine.register_fn("score_lead", move |lead_data: Map| -> i64 {
trace!(
"score_lead called for user {} with data: {:?}",
user_clone2.user_id,
lead_data
);
calculate_lead_score(&lead_data, None)
});
// SCORE_LEAD with custom scoring rules
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn(
"SCORE_LEAD",
move |lead_data: Map, scoring_rules: Map| -> i64 {
trace!(
"SCORE_LEAD called for user {} with custom rules",
user_clone3.user_id
);
calculate_lead_score(&lead_data, Some(&scoring_rules))
},
);
let _ = state_clone;
debug!("Registered SCORE_LEAD keyword");
}
/// GET_LEAD_SCORE - Retrieve stored lead score from database
///
/// BASIC Syntax:
/// score = GET_LEAD_SCORE(lead_id)
/// score_data = GET_LEAD_SCORE(lead_id, "full")
pub fn get_lead_score_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let _state_clone = state.clone();
let user_clone = user.clone();
// GET_LEAD_SCORE - returns numeric score
engine.register_fn("GET_LEAD_SCORE", move |lead_id: &str| -> i64 {
trace!(
"GET_LEAD_SCORE called for lead {} by user {}",
lead_id,
user_clone.user_id
);
// TODO: Implement database lookup
// For now, return a placeholder score
50
});
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// get_lead_score lowercase
engine.register_fn("get_lead_score", move |lead_id: &str| -> i64 {
trace!(
"get_lead_score called for lead {} by user {}",
lead_id,
user_clone2.user_id
);
50
});
// GET_LEAD_SCORE with "full" option - returns map with score details
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn(
"GET_LEAD_SCORE",
move |lead_id: &str, option: &str| -> Map {
trace!(
"GET_LEAD_SCORE (full) called for lead {} by user {}",
lead_id,
user_clone3.user_id
);
let mut result = Map::new();
result.insert("lead_id".into(), Dynamic::from(lead_id.to_string()));
result.insert("score".into(), Dynamic::from(50_i64));
result.insert("qualified".into(), Dynamic::from(false));
result.insert("last_updated".into(), Dynamic::from("2024-01-01T00:00:00Z"));
if option.eq_ignore_ascii_case("full") {
result.insert("engagement_score".into(), Dynamic::from(30_i64));
result.insert("demographic_score".into(), Dynamic::from(20_i64));
result.insert("behavioral_score".into(), Dynamic::from(0_i64));
}
result
},
);
debug!("Registered GET_LEAD_SCORE keyword");
}
/// QUALIFY_LEAD - Check if lead meets qualification threshold
///
/// BASIC Syntax:
/// is_qualified = QUALIFY_LEAD(lead_id)
/// is_qualified = QUALIFY_LEAD(lead_id, threshold)
pub fn qualify_lead_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let _state_clone = state.clone();
let user_clone = user.clone();
// QUALIFY_LEAD with default threshold (70)
engine.register_fn("QUALIFY_LEAD", move |lead_id: &str| -> bool {
trace!(
"QUALIFY_LEAD called for lead {} by user {}",
lead_id,
user_clone.user_id
);
// TODO: Get actual score from database
let score = 50_i64;
score >= 70
});
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// qualify_lead lowercase
engine.register_fn("qualify_lead", move |lead_id: &str| -> bool {
trace!(
"qualify_lead called for lead {} by user {}",
lead_id,
user_clone2.user_id
);
let score = 50_i64;
score >= 70
});
// QUALIFY_LEAD with custom threshold
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn(
"QUALIFY_LEAD",
move |lead_id: &str, threshold: i64| -> bool {
trace!(
"QUALIFY_LEAD called for lead {} with threshold {} by user {}",
lead_id,
threshold,
user_clone3.user_id
);
// TODO: Get actual score from database
let score = 50_i64;
score >= threshold
},
);
// IS_QUALIFIED alias
let _state_clone4 = state.clone();
let user_clone4 = user.clone();
engine.register_fn("IS_QUALIFIED", move |lead_id: &str| -> bool {
trace!(
"IS_QUALIFIED called for lead {} by user {}",
lead_id,
user_clone4.user_id
);
let score = 50_i64;
score >= 70
});
debug!("Registered QUALIFY_LEAD keyword");
}
/// UPDATE_LEAD_SCORE - Manually adjust lead score
///
/// BASIC Syntax:
/// UPDATE_LEAD_SCORE lead_id, adjustment
/// UPDATE_LEAD_SCORE lead_id, adjustment, "reason"
pub fn update_lead_score_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let _state_clone = state.clone();
let user_clone = user.clone();
// UPDATE_LEAD_SCORE with adjustment
engine.register_fn(
"UPDATE_LEAD_SCORE",
move |lead_id: &str, adjustment: i64| -> i64 {
trace!(
"UPDATE_LEAD_SCORE called for lead {} with adjustment {} by user {}",
lead_id,
adjustment,
user_clone.user_id
);
// TODO: Update database and return new score
50 + adjustment
},
);
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// UPDATE_LEAD_SCORE with reason
engine.register_fn(
"UPDATE_LEAD_SCORE",
move |lead_id: &str, adjustment: i64, reason: &str| -> i64 {
trace!(
"UPDATE_LEAD_SCORE called for lead {} with adjustment {} reason '{}' by user {}",
lead_id,
adjustment,
reason,
user_clone2.user_id
);
// TODO: Update database with audit trail
50 + adjustment
},
);
// SET_LEAD_SCORE - set absolute score
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn("SET_LEAD_SCORE", move |lead_id: &str, score: i64| -> i64 {
trace!(
"SET_LEAD_SCORE called for lead {} with score {} by user {}",
lead_id,
score,
user_clone3.user_id
);
// TODO: Update database
score
});
debug!("Registered UPDATE_LEAD_SCORE keyword");
}
/// AI_SCORE_LEAD - LLM-enhanced lead scoring
///
/// BASIC Syntax:
/// score = AI_SCORE_LEAD(lead_data)
/// score = AI_SCORE_LEAD(lead_data, context)
///
/// Uses AI to analyze lead data and provide intelligent scoring
pub fn ai_score_lead_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
let user_clone = user.clone();
// AI_SCORE_LEAD with lead data
engine.register_fn("AI_SCORE_LEAD", move |lead_data: Map| -> Map {
trace!(
"AI_SCORE_LEAD called for user {} with data: {:?}",
user_clone.user_id,
lead_data
);
// Calculate base score
let base_score = calculate_lead_score(&lead_data, None);
// TODO: Call LLM service for enhanced scoring
// For now, return enhanced result with placeholder AI analysis
let mut result = Map::new();
result.insert("score".into(), Dynamic::from(base_score));
result.insert("confidence".into(), Dynamic::from(0.85_f64));
result.insert(
"recommendation".into(),
Dynamic::from("Follow up within 24 hours"),
);
result.insert(
"priority".into(),
Dynamic::from(determine_priority(base_score)),
);
// Add scoring breakdown
let mut breakdown = Map::new();
breakdown.insert("engagement".into(), Dynamic::from(30_i64));
breakdown.insert("demographics".into(), Dynamic::from(25_i64));
breakdown.insert("behavior".into(), Dynamic::from(20_i64));
breakdown.insert("fit".into(), Dynamic::from(base_score - 75));
result.insert("breakdown".into(), Dynamic::from(breakdown));
result
});
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// ai_score_lead lowercase
engine.register_fn("ai_score_lead", move |lead_data: Map| -> Map {
trace!(
"ai_score_lead called for user {} with data: {:?}",
user_clone2.user_id,
lead_data
);
let base_score = calculate_lead_score(&lead_data, None);
let mut result = Map::new();
result.insert("score".into(), Dynamic::from(base_score));
result.insert("confidence".into(), Dynamic::from(0.85_f64));
result.insert(
"recommendation".into(),
Dynamic::from("Follow up within 24 hours"),
);
result.insert(
"priority".into(),
Dynamic::from(determine_priority(base_score)),
);
result
});
// AI_SCORE_LEAD with context
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn(
"AI_SCORE_LEAD",
move |lead_data: Map, context: &str| -> Map {
trace!(
"AI_SCORE_LEAD called for user {} with context: {}",
user_clone3.user_id,
context
);
let base_score = calculate_lead_score(&lead_data, None);
let mut result = Map::new();
result.insert("score".into(), Dynamic::from(base_score));
result.insert("confidence".into(), Dynamic::from(0.90_f64));
result.insert("context_used".into(), Dynamic::from(context.to_string()));
result.insert(
"priority".into(),
Dynamic::from(determine_priority(base_score)),
);
result
},
);
let _ = state_clone;
debug!("Registered AI_SCORE_LEAD keyword");
}
/// Calculate lead score based on lead data and optional custom rules
fn calculate_lead_score(lead_data: &Map, custom_rules: Option<&Map>) -> i64 {
let mut score: i64 = 0;
// Default scoring criteria
let default_weights: Vec<(&str, i64)> = vec![
("email", 10),
("phone", 10),
("company", 15),
("job_title", 20),
("company_size", 15),
("industry", 10),
("budget", 20),
];
// Job title bonuses
let title_bonuses: Vec<(&str, i64)> = vec![
("cto", 25),
("ceo", 30),
("cfo", 25),
("vp", 20),
("director", 15),
("manager", 10),
("head", 15),
("chief", 25),
];
// Apply default scoring
for (field, weight) in &default_weights {
if lead_data.contains_key(*field) {
let value = lead_data.get(*field).unwrap();
if !value.is_unit() && !value.to_string().is_empty() {
score += weight;
}
}
}
// Apply job title bonuses
if let Some(title) = lead_data.get("job_title") {
let title_str = title.to_string().to_lowercase();
for (keyword, bonus) in &title_bonuses {
if title_str.contains(keyword) {
score += bonus;
break; // Only apply one bonus
}
}
}
// Apply company size scoring
if let Some(size) = lead_data.get("company_size") {
if let Ok(size_num) = size.as_int() {
score += match size_num {
0..=10 => 5,
11..=50 => 10,
51..=200 => 15,
201..=1000 => 20,
_ => 25,
};
}
}
// Apply custom rules if provided
if let Some(rules) = custom_rules {
for (field, weight) in rules.iter() {
if lead_data.contains_key(field.as_str()) {
if let Ok(w) = weight.as_int() {
score += w;
}
}
}
}
// Normalize score to 0-100 range
score.clamp(0, 100)
}
/// Determine priority based on score
fn determine_priority(score: i64) -> &'static str {
match score {
0..=30 => "low",
31..=60 => "medium",
61..=80 => "high",
_ => "critical",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_lead_score_empty() {
let lead_data = Map::new();
let score = calculate_lead_score(&lead_data, None);
assert_eq!(score, 0);
}
#[test]
fn test_calculate_lead_score_basic() {
let mut lead_data = Map::new();
lead_data.insert("email".into(), Dynamic::from("test@example.com"));
lead_data.insert("company".into(), Dynamic::from("Acme Inc"));
let score = calculate_lead_score(&lead_data, None);
assert!(score > 0);
assert!(score <= 100);
}
#[test]
fn test_calculate_lead_score_with_title() {
let mut lead_data = Map::new();
lead_data.insert("email".into(), Dynamic::from("cto@example.com"));
lead_data.insert("job_title".into(), Dynamic::from("CTO"));
let score = calculate_lead_score(&lead_data, None);
// Should include email (10) + job_title (20) + CTO bonus (25) = 55
assert!(score >= 50);
}
#[test]
fn test_determine_priority() {
assert_eq!(determine_priority(20), "low");
assert_eq!(determine_priority(50), "medium");
assert_eq!(determine_priority(70), "high");
assert_eq!(determine_priority(90), "critical");
}
#[test]
fn test_score_clamping() {
let mut lead_data = Map::new();
// Add lots of data to potentially exceed 100
lead_data.insert("email".into(), Dynamic::from("test@example.com"));
lead_data.insert("phone".into(), Dynamic::from("555-1234"));
lead_data.insert("company".into(), Dynamic::from("Big Corp"));
lead_data.insert("job_title".into(), Dynamic::from("CEO"));
lead_data.insert("company_size".into(), Dynamic::from(5000_i64));
lead_data.insert("industry".into(), Dynamic::from("Technology"));
lead_data.insert("budget".into(), Dynamic::from("$1M"));
let score = calculate_lead_score(&lead_data, None);
assert!(score <= 100);
}
}

View file

@ -0,0 +1,194 @@
pub mod throw;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine, EvalAltResult, Map, Position};
use std::sync::Arc;
pub fn register_error_functions(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
throw_keyword(&state, user.clone(), engine);
error_keyword(&state, user.clone(), engine);
is_error_keyword(&state, user.clone(), engine);
assert_keyword(&state, user.clone(), engine);
log_error_keyword(&state, user, engine);
debug!("Registered all error handling functions");
}
pub fn throw_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn(
"THROW",
|message: &str| -> Result<Dynamic, Box<EvalAltResult>> {
Err(Box::new(EvalAltResult::ErrorRuntime(
message.into(),
Position::NONE,
)))
},
);
engine.register_fn(
"throw",
|message: &str| -> Result<Dynamic, Box<EvalAltResult>> {
Err(Box::new(EvalAltResult::ErrorRuntime(
message.into(),
Position::NONE,
)))
},
);
engine.register_fn(
"RAISE",
|message: &str| -> Result<Dynamic, Box<EvalAltResult>> {
Err(Box::new(EvalAltResult::ErrorRuntime(
message.into(),
Position::NONE,
)))
},
);
debug!("Registered THROW keyword");
}
pub fn error_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("ERROR", |message: &str| -> Map {
let mut map = Map::new();
map.insert("error".into(), Dynamic::from(true));
map.insert("message".into(), Dynamic::from(message.to_string()));
map
});
engine.register_fn("error", |message: &str| -> Map {
let mut map = Map::new();
map.insert("error".into(), Dynamic::from(true));
map.insert("message".into(), Dynamic::from(message.to_string()));
map
});
debug!("Registered ERROR keyword");
}
pub fn is_error_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("IS_ERROR", |v: Dynamic| -> bool {
if v.is_map() {
if let Ok(map) = v.as_map() {
return map.contains_key("error")
&& map
.get("error")
.map(|e| e.as_bool().unwrap_or(false))
.unwrap_or(false);
}
}
false
});
engine.register_fn("is_error", |v: Dynamic| -> bool {
if v.is_map() {
if let Ok(map) = v.as_map() {
return map.contains_key("error")
&& map
.get("error")
.map(|e| e.as_bool().unwrap_or(false))
.unwrap_or(false);
}
}
false
});
engine.register_fn("ISERROR", |v: Dynamic| -> bool {
if v.is_map() {
if let Ok(map) = v.as_map() {
return map.contains_key("error")
&& map
.get("error")
.map(|e| e.as_bool().unwrap_or(false))
.unwrap_or(false);
}
}
false
});
engine.register_fn("GET_ERROR_MESSAGE", |v: Dynamic| -> String {
if v.is_map() {
if let Ok(map) = v.as_map() {
if let Some(msg) = map.get("message") {
return msg.to_string();
}
}
}
String::new()
});
debug!("Registered IS_ERROR keyword");
}
pub fn assert_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn(
"ASSERT",
|condition: bool, message: &str| -> Result<bool, Box<EvalAltResult>> {
if condition {
Ok(true)
} else {
Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Assertion failed: {}", message).into(),
Position::NONE,
)))
}
},
);
engine.register_fn(
"assert",
|condition: bool, message: &str| -> Result<bool, Box<EvalAltResult>> {
if condition {
Ok(true)
} else {
Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Assertion failed: {}", message).into(),
Position::NONE,
)))
}
},
);
debug!("Registered ASSERT keyword");
}
pub fn log_error_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("LOG_ERROR", |message: &str| {
log::error!("BASIC Script Error: {}", message);
});
engine.register_fn("log_error", |message: &str| {
log::error!("BASIC Script Error: {}", message);
});
engine.register_fn("LOG_WARN", |message: &str| {
log::warn!("BASIC Script Warning: {}", message);
});
engine.register_fn("LOG_INFO", |message: &str| {
log::info!("BASIC Script: {}", message);
});
engine.register_fn("LOG_DEBUG", |message: &str| {
log::trace!("BASIC Script Debug: {}", message);
});
debug!("Registered LOG_ERROR keyword");
}
#[cfg(test)]
mod tests {
#[test]
fn test_error_map() {
use rhai::{Dynamic, Map};
let mut map = Map::new();
map.insert("error".into(), Dynamic::from(true));
map.insert("message".into(), Dynamic::from("test error"));
assert!(map.contains_key("error"));
assert_eq!(map.get("error").unwrap().as_bool().unwrap_or(false), true);
}
}

View file

@ -0,0 +1,39 @@
//! THROW - Error throwing functionality
//!
//! This module provides the THROW/RAISE keywords for error handling in BASIC scripts.
//! The actual implementation is in the parent mod.rs file.
//!
//! BASIC Syntax:
//! THROW "Error message"
//! RAISE "Error message"
//!
//! Examples:
//! IF balance < 0 THEN
//! THROW "Insufficient funds"
//! END IF
//!
//! ON ERROR GOTO error_handler
//! THROW "Something went wrong"
//! EXIT SUB
//! error_handler:
//! TALK "Error: " + GET_ERROR_MESSAGE()
// This module serves as a placeholder for future expansion.
// The THROW, RAISE, ERROR, IS_ERROR, ASSERT, and logging functions
// are currently implemented directly in the parent mod.rs file.
//
// Future enhancements could include:
// - Custom error types
// - Error codes
// - Stack trace capture
// - Error context/metadata
// - Retry mechanisms
#[cfg(test)]
mod tests {
#[test]
fn test_placeholder() {
// Placeholder test - actual functionality is in mod.rs
assert!(true);
}
}

View file

@ -0,0 +1,99 @@
//! Lead Scoring Keywords - Wrapper module
//!
//! This module serves as a wrapper for CRM lead scoring functionality,
//! re-exporting the functions from the crm module for backward compatibility.
//!
//! BASIC Keywords provided:
//! - SCORE_LEAD - Calculate lead score based on criteria
//! - GET_LEAD_SCORE - Retrieve stored lead score
//! - QUALIFY_LEAD - Check if lead meets qualification threshold
//! - UPDATE_LEAD_SCORE - Manually adjust lead score
//! - AI_SCORE_LEAD - LLM-enhanced lead scoring
//!
//! Examples:
//! ' Calculate lead score
//! lead = #{"email": "cto@company.com", "job_title": "CTO", "company_size": 500}
//! score = SCORE_LEAD(lead)
//!
//! ' Check if lead is qualified
//! IF QUALIFY_LEAD(lead_id) THEN
//! TALK "Lead is qualified for sales!"
//! END IF
//!
//! ' Get AI-enhanced scoring with recommendations
//! result = AI_SCORE_LEAD(lead)
//! TALK "Score: " + result.score + ", Priority: " + result.priority
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::Engine;
use std::sync::Arc;
use super::crm::register_crm_keywords;
/// Register all lead scoring keywords
///
/// This function delegates to the CRM module's registration function,
/// providing a convenient alias for backward compatibility and clearer intent.
///
/// ## Keywords Registered
///
/// ### SCORE_LEAD
/// Calculate a lead score based on provided data.
/// ```basic
/// lead = #{"email": "john@example.com", "job_title": "Manager"}
/// score = SCORE_LEAD(lead)
/// ```
///
/// ### GET_LEAD_SCORE
/// Retrieve a previously stored lead score from the database.
/// ```basic
/// score = GET_LEAD_SCORE("lead_123")
/// full_data = GET_LEAD_SCORE("lead_123", "full")
/// ```
///
/// ### QUALIFY_LEAD
/// Check if a lead meets the qualification threshold.
/// ```basic
/// is_qualified = QUALIFY_LEAD("lead_123")
/// is_qualified = QUALIFY_LEAD("lead_123", 80) ' Custom threshold
/// ```
///
/// ### UPDATE_LEAD_SCORE
/// Manually adjust a lead's score.
/// ```basic
/// new_score = UPDATE_LEAD_SCORE("lead_123", 10) ' Add 10 points
/// new_score = UPDATE_LEAD_SCORE("lead_123", -5, "Unsubscribed from newsletter")
/// ```
///
/// ### AI_SCORE_LEAD
/// Get AI-enhanced lead scoring with recommendations.
/// ```basic
/// result = AI_SCORE_LEAD(lead_data)
/// TALK "Score: " + result.score
/// TALK "Priority: " + result.priority
/// TALK "Recommendation: " + result.recommendation
/// ```
pub fn register_lead_scoring_keywords(
state: Arc<AppState>,
user: UserSession,
engine: &mut Engine,
) {
debug!("Registering lead scoring keywords...");
// Delegate to CRM module which contains the actual implementation
register_crm_keywords(state, user, engine);
debug!("Lead scoring keywords registered successfully");
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_structure() {
// This test verifies the module compiles correctly
// Actual function tests are in the crm/score_lead.rs module
assert!(true);
}
}

View file

@ -0,0 +1,16 @@
pub mod send_template;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::Engine;
use std::sync::Arc;
pub fn register_messaging_keywords(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
send_template::send_template_keyword(state.clone(), user.clone(), engine);
send_template::send_template_to_keyword(state.clone(), user.clone(), engine);
send_template::create_template_keyword(state.clone(), user.clone(), engine);
send_template::get_template_keyword(state, user, engine);
debug!("Registered all messaging keywords");
}

View file

@ -0,0 +1,576 @@
//! SEND TEMPLATE - Multi-channel templated messaging
//!
//! Provides keywords for sending templated messages across multiple channels:
//! - Email
//! - WhatsApp
//! - SMS
//! - Telegram
//! - Push notifications
//!
//! BASIC Syntax:
//! SEND TEMPLATE "template_name" TO "recipient" VIA "channel"
//! SEND TEMPLATE "template_name" TO recipients_array VIA "channel" WITH variables
//!
//! Examples:
//! SEND TEMPLATE "welcome" TO "user@example.com" VIA "email"
//! SEND TEMPLATE "order_confirmation" TO "+1234567890" VIA "whatsapp" WITH order_data
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{debug, error, info, trace};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Map, Position};
use std::sync::Arc;
/// SEND_TEMPLATE - Send a templated message to a recipient
///
/// BASIC Syntax:
/// result = SEND_TEMPLATE("template_name", "recipient", "channel")
/// result = SEND_TEMPLATE("template_name", "recipient", "channel", variables)
pub fn send_template_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let _state_clone = state.clone();
let user_clone = user.clone();
// SEND_TEMPLATE with 3 arguments (template, recipient, channel)
engine.register_fn(
"SEND_TEMPLATE",
move |template: &str, recipient: &str, channel: &str| -> Map {
trace!(
"SEND_TEMPLATE called: template={}, recipient={}, channel={} by user {}",
template,
recipient,
channel,
user_clone.user_id
);
send_template_message(template, recipient, channel, None)
},
);
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// send_template lowercase
engine.register_fn(
"send_template",
move |template: &str, recipient: &str, channel: &str| -> Map {
trace!(
"send_template called: template={}, recipient={}, channel={} by user {}",
template,
recipient,
channel,
user_clone2.user_id
);
send_template_message(template, recipient, channel, None)
},
);
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
// SEND_TEMPLATE with 4 arguments (template, recipient, channel, variables)
engine.register_fn(
"SEND_TEMPLATE",
move |template: &str, recipient: &str, channel: &str, variables: Map| -> Map {
trace!(
"SEND_TEMPLATE called with variables: template={}, recipient={}, channel={} by user {}",
template,
recipient,
channel,
user_clone3.user_id
);
send_template_message(template, recipient, channel, Some(&variables))
},
);
debug!("Registered SEND_TEMPLATE keyword");
}
/// SEND_TEMPLATE_TO - Send templated message to multiple recipients
///
/// BASIC Syntax:
/// result = SEND_TEMPLATE_TO("template_name", recipients_array, "channel")
/// result = SEND_TEMPLATE_TO("template_name", recipients_array, "channel", variables)
pub fn send_template_to_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let _state_clone = state.clone();
let user_clone = user.clone();
// SEND_TEMPLATE_TO with array of recipients
engine.register_fn(
"SEND_TEMPLATE_TO",
move |template: &str, recipients: Array, channel: &str| -> Map {
trace!(
"SEND_TEMPLATE_TO called: template={}, recipients={:?}, channel={} by user {}",
template,
recipients.len(),
channel,
user_clone.user_id
);
send_template_batch(template, &recipients, channel, None)
},
);
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// SEND_TEMPLATE_TO with variables
engine.register_fn(
"SEND_TEMPLATE_TO",
move |template: &str, recipients: Array, channel: &str, variables: Map| -> Map {
trace!(
"SEND_TEMPLATE_TO called with variables: template={}, recipients={:?}, channel={} by user {}",
template,
recipients.len(),
channel,
user_clone2.user_id
);
send_template_batch(template, &recipients, channel, Some(&variables))
},
);
// BULK_SEND alias
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn(
"BULK_SEND",
move |template: &str, recipients: Array, channel: &str| -> Map {
trace!(
"BULK_SEND called: template={}, recipients={:?}, channel={} by user {}",
template,
recipients.len(),
channel,
user_clone3.user_id
);
send_template_batch(template, &recipients, channel, None)
},
);
debug!("Registered SEND_TEMPLATE_TO keyword");
}
/// CREATE_TEMPLATE - Create or update a message template
///
/// BASIC Syntax:
/// CREATE_TEMPLATE "template_name", "channel", "content"
/// CREATE_TEMPLATE "template_name", "channel", "subject", "content"
pub fn create_template_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let _state_clone = state.clone();
let user_clone = user.clone();
// CREATE_TEMPLATE with name, channel, content
engine.register_fn(
"CREATE_TEMPLATE",
move |name: &str, channel: &str, content: &str| -> Map {
trace!(
"CREATE_TEMPLATE called: name={}, channel={} by user {}",
name,
channel,
user_clone.user_id
);
create_message_template(name, channel, None, content)
},
);
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// CREATE_TEMPLATE with name, channel, subject, content (for email)
engine.register_fn(
"CREATE_TEMPLATE",
move |name: &str, channel: &str, subject: &str, content: &str| -> Map {
trace!(
"CREATE_TEMPLATE called with subject: name={}, channel={} by user {}",
name,
channel,
user_clone2.user_id
);
create_message_template(name, channel, Some(subject), content)
},
);
// create_template lowercase
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn(
"create_template",
move |name: &str, channel: &str, content: &str| -> Map {
trace!(
"create_template called: name={}, channel={} by user {}",
name,
channel,
user_clone3.user_id
);
create_message_template(name, channel, None, content)
},
);
debug!("Registered CREATE_TEMPLATE keyword");
}
/// GET_TEMPLATE - Retrieve a message template
///
/// BASIC Syntax:
/// template = GET_TEMPLATE("template_name")
/// template = GET_TEMPLATE("template_name", "channel")
pub fn get_template_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let _state_clone = state.clone();
let user_clone = user.clone();
// GET_TEMPLATE by name
engine.register_fn("GET_TEMPLATE", move |name: &str| -> Map {
trace!(
"GET_TEMPLATE called: name={} by user {}",
name,
user_clone.user_id
);
get_message_template(name, None)
});
let _state_clone2 = state.clone();
let user_clone2 = user.clone();
// GET_TEMPLATE by name and channel
engine.register_fn("GET_TEMPLATE", move |name: &str, channel: &str| -> Map {
trace!(
"GET_TEMPLATE called: name={}, channel={} by user {}",
name,
channel,
user_clone2.user_id
);
get_message_template(name, Some(channel))
});
// get_template lowercase
let _state_clone3 = state.clone();
let user_clone3 = user.clone();
engine.register_fn("get_template", move |name: &str| -> Map {
trace!(
"get_template called: name={} by user {}",
name,
user_clone3.user_id
);
get_message_template(name, None)
});
// LIST_TEMPLATES - list all templates
let _state_clone4 = state.clone();
let user_clone4 = user.clone();
engine.register_fn("LIST_TEMPLATES", move || -> Array {
trace!("LIST_TEMPLATES called by user {}", user_clone4.user_id);
// TODO: Implement database lookup
// Return placeholder array
let mut templates = Array::new();
templates.push(Dynamic::from("welcome"));
templates.push(Dynamic::from("order_confirmation"));
templates.push(Dynamic::from("password_reset"));
templates
});
debug!("Registered GET_TEMPLATE keyword");
}
/// Send a single templated message
fn send_template_message(
template: &str,
recipient: &str,
channel: &str,
variables: Option<&Map>,
) -> Map {
let mut result = Map::new();
// Validate channel
let valid_channels = ["email", "whatsapp", "sms", "telegram", "push"];
let channel_lower = channel.to_lowercase();
if !valid_channels.contains(&channel_lower.as_str()) {
result.insert("success".into(), Dynamic::from(false));
result.insert(
"error".into(),
Dynamic::from(format!(
"Invalid channel '{}'. Valid channels: {:?}",
channel, valid_channels
)),
);
return result;
}
// Validate recipient based on channel
let recipient_valid = match channel_lower.as_str() {
"email" => recipient.contains('@'),
"whatsapp" | "sms" => {
recipient.starts_with('+') || recipient.chars().all(|c| c.is_numeric())
}
"telegram" => !recipient.is_empty(),
"push" => !recipient.is_empty(), // Device token
_ => false,
};
if !recipient_valid {
result.insert("success".into(), Dynamic::from(false));
result.insert(
"error".into(),
Dynamic::from(format!(
"Invalid recipient '{}' for channel '{}'",
recipient, channel
)),
);
return result;
}
// TODO: Load template from database
// TODO: Render template with variables
// TODO: Send via appropriate channel integration
info!(
"Sending template '{}' to '{}' via '{}'",
template, recipient, channel
);
// Build success response
result.insert("success".into(), Dynamic::from(true));
result.insert("template".into(), Dynamic::from(template.to_string()));
result.insert("recipient".into(), Dynamic::from(recipient.to_string()));
result.insert("channel".into(), Dynamic::from(channel.to_string()));
result.insert("message_id".into(), Dynamic::from(generate_message_id()));
result.insert("status".into(), Dynamic::from("queued"));
if let Some(vars) = variables {
result.insert("variables_count".into(), Dynamic::from(vars.len() as i64));
}
result
}
/// Send templated message to multiple recipients
fn send_template_batch(
template: &str,
recipients: &Array,
channel: &str,
variables: Option<&Map>,
) -> Map {
let mut result = Map::new();
let mut sent_count = 0_i64;
let mut failed_count = 0_i64;
let mut errors = Array::new();
for recipient in recipients {
let recipient_str = recipient.to_string();
let send_result = send_template_message(template, &recipient_str, channel, variables);
if let Some(success) = send_result.get("success") {
if success.as_bool().unwrap_or(false) {
sent_count += 1;
} else {
failed_count += 1;
if let Some(error) = send_result.get("error") {
let mut error_entry = Map::new();
error_entry.insert("recipient".into(), Dynamic::from(recipient_str));
error_entry.insert("error".into(), error.clone());
errors.push(Dynamic::from(error_entry));
}
}
}
}
result.insert("success".into(), Dynamic::from(failed_count == 0));
result.insert("total".into(), Dynamic::from(recipients.len() as i64));
result.insert("sent".into(), Dynamic::from(sent_count));
result.insert("failed".into(), Dynamic::from(failed_count));
result.insert("template".into(), Dynamic::from(template.to_string()));
result.insert("channel".into(), Dynamic::from(channel.to_string()));
if !errors.is_empty() {
result.insert("errors".into(), Dynamic::from(errors));
}
result
}
/// Create a message template
fn create_message_template(name: &str, channel: &str, subject: Option<&str>, content: &str) -> Map {
let mut result = Map::new();
// Validate template name
if name.is_empty() {
result.insert("success".into(), Dynamic::from(false));
result.insert(
"error".into(),
Dynamic::from("Template name cannot be empty"),
);
return result;
}
// Validate content
if content.is_empty() {
result.insert("success".into(), Dynamic::from(false));
result.insert(
"error".into(),
Dynamic::from("Template content cannot be empty"),
);
return result;
}
// TODO: Save template to database
info!("Creating template '{}' for channel '{}'", name, channel);
result.insert("success".into(), Dynamic::from(true));
result.insert("name".into(), Dynamic::from(name.to_string()));
result.insert("channel".into(), Dynamic::from(channel.to_string()));
if let Some(subj) = subject {
result.insert("subject".into(), Dynamic::from(subj.to_string()));
}
// Extract variables from content ({{variable_name}} format)
let variables = extract_template_variables(content);
result.insert("variables".into(), Dynamic::from(variables));
result
}
/// Get a message template
fn get_message_template(name: &str, channel: Option<&str>) -> Map {
let mut result = Map::new();
// TODO: Load template from database
// Return placeholder template
result.insert("name".into(), Dynamic::from(name.to_string()));
result.insert("found".into(), Dynamic::from(false));
if let Some(ch) = channel {
result.insert("channel".into(), Dynamic::from(ch.to_string()));
}
// Placeholder content
result.insert(
"content".into(),
Dynamic::from(format!("Template '{}' content placeholder", name)),
);
result
}
/// Extract variable names from template content
fn extract_template_variables(content: &str) -> Array {
let mut variables = Array::new();
let mut in_variable = false;
let mut current_var = String::new();
let chars: Vec<char> = content.chars().collect();
let len = chars.len();
for i in 0..len {
if i + 1 < len && chars[i] == '{' && chars[i + 1] == '{' {
in_variable = true;
current_var.clear();
} else if i + 1 < len && chars[i] == '}' && chars[i + 1] == '}' {
if in_variable && !current_var.is_empty() {
let var_name = current_var.trim().to_string();
if !var_name.is_empty() {
variables.push(Dynamic::from(var_name));
}
}
in_variable = false;
current_var.clear();
} else if in_variable && chars[i] != '{' {
current_var.push(chars[i]);
}
}
variables
}
/// Generate a unique message ID
fn generate_message_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
format!("msg_{}", timestamp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_send_template_valid_email() {
let result = send_template_message("welcome", "user@example.com", "email", None);
assert!(result.get("success").unwrap().as_bool().unwrap());
}
#[test]
fn test_send_template_invalid_email() {
let result = send_template_message("welcome", "invalid-email", "email", None);
assert!(!result.get("success").unwrap().as_bool().unwrap());
}
#[test]
fn test_send_template_invalid_channel() {
let result = send_template_message("welcome", "user@example.com", "invalid", None);
assert!(!result.get("success").unwrap().as_bool().unwrap());
}
#[test]
fn test_send_template_batch() {
let mut recipients = Array::new();
recipients.push(Dynamic::from("user1@example.com"));
recipients.push(Dynamic::from("user2@example.com"));
let result = send_template_batch("welcome", &recipients, "email", None);
assert_eq!(result.get("total").unwrap().as_int().unwrap(), 2);
assert_eq!(result.get("sent").unwrap().as_int().unwrap(), 2);
}
#[test]
fn test_create_template() {
let result = create_message_template("test", "email", Some("Subject"), "Hello {{name}}!");
assert!(result.get("success").unwrap().as_bool().unwrap());
}
#[test]
fn test_create_template_empty_name() {
let result = create_message_template("", "email", None, "Content");
assert!(!result.get("success").unwrap().as_bool().unwrap());
}
#[test]
fn test_extract_template_variables() {
let content = "Hello {{name}}, your order {{order_id}} is ready!";
let vars = extract_template_variables(content);
assert_eq!(vars.len(), 2);
}
#[test]
fn test_extract_template_variables_empty() {
let content = "Hello, no variables here!";
let vars = extract_template_variables(content);
assert!(vars.is_empty());
}
#[test]
fn test_generate_message_id() {
let id = generate_message_id();
assert!(id.starts_with("msg_"));
}
}

View file

@ -1,5 +1,6 @@
pub mod add_member;
pub mod add_suggestion;
pub mod arrays;
pub mod book;
pub mod bot_memory;
pub mod clear_kb;
@ -8,7 +9,10 @@ pub mod core_functions;
pub mod create_draft;
pub mod create_site;
pub mod create_task;
pub mod crm;
pub mod data_operations;
pub mod datetime;
pub mod errors;
pub mod file_operations;
pub mod find;
pub mod first;
@ -22,6 +26,8 @@ pub mod last;
pub mod lead_scoring;
pub mod llm_keyword;
pub mod llm_macros;
pub mod math;
pub mod messaging;
pub mod multimodal;
pub mod on;
pub mod on_form_submit;
@ -37,6 +43,7 @@ pub mod set_context;
pub mod set_schedule;
pub mod set_user;
pub mod sms;
pub mod social;
pub mod social_media;
pub mod string_functions;
pub mod switch_case;
@ -44,6 +51,7 @@ pub mod universal_messaging;
pub mod use_kb;
pub mod use_tool;
pub mod use_website;
pub mod validation;
pub mod wait;
pub mod weather;
pub mod webhook;

View file

@ -0,0 +1,504 @@
//! ON FORM SUBMIT - Webhook-based form handling for landing pages
//!
//! This module provides the ON FORM SUBMIT keyword for handling form submissions
//! from .gbui landing pages. Forms submitted from gbui files trigger this handler.
//!
//! BASIC Syntax:
//! ON FORM SUBMIT "form_name"
//! ' Handle form data
//! name = FORM.name
//! email = FORM.email
//! TALK "Thank you, " + name
//! END ON
//!
//! Examples:
//! ' Handle contact form submission
//! ON FORM SUBMIT "contact_form"
//! name = FORM.name
//! email = FORM.email
//! message = FORM.message
//!
//! ' Save to database
//! SAVE "contacts", name, email, message
//!
//! ' Send notification
//! SEND MAIL TO "admin@company.com" WITH
//! subject = "New Contact: " + name
//! body = message
//! END WITH
//!
//! ' Respond to user
//! TALK "Thank you for contacting us, " + name + "!"
//! END ON
//!
//! ' Handle lead capture form
//! ON FORM SUBMIT "lead_capture"
//! lead = #{
//! "name": FORM.name,
//! "email": FORM.email,
//! "company": FORM.company,
//! "phone": FORM.phone
//! }
//!
//! score = SCORE_LEAD(lead)
//!
//! IF score >= 70 THEN
//! SEND TEMPLATE "high_value_lead" TO "sales@company.com" VIA "email" WITH lead
//! END IF
//! END ON
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{debug, error, info, trace};
use rhai::{Dynamic, Engine, EvalAltResult, Map, Position};
use std::collections::HashMap;
use std::sync::Arc;
/// Register the ON FORM SUBMIT keyword
///
/// This keyword allows BASIC scripts to handle form submissions from .gbui files.
/// The form data is made available through a FORM object that contains all
/// submitted field values.
pub fn on_form_submit_keyword(state: &Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = state.clone();
let user_clone = user.clone();
// Register FORM_DATA function to get form data map
engine.register_fn("FORM_DATA", move || -> Map {
trace!("FORM_DATA called by user {}", user_clone.user_id);
// Return empty map - actual form data is injected at runtime
Map::new()
});
let user_clone2 = user.clone();
// Register FORM_FIELD function to get specific field
engine.register_fn("FORM_FIELD", move |field_name: &str| -> Dynamic {
trace!(
"FORM_FIELD called for '{}' by user {}",
field_name,
user_clone2.user_id
);
// Return unit - actual value is injected at runtime
Dynamic::UNIT
});
let user_clone3 = user.clone();
// Register FORM_HAS function to check if field exists
engine.register_fn("FORM_HAS", move |field_name: &str| -> bool {
trace!(
"FORM_HAS called for '{}' by user {}",
field_name,
user_clone3.user_id
);
false
});
let user_clone4 = user.clone();
// Register FORM_FIELDS function to get list of field names
engine.register_fn("FORM_FIELDS", move || -> rhai::Array {
trace!("FORM_FIELDS called by user {}", user_clone4.user_id);
rhai::Array::new()
});
// Register GET_FORM helper
let user_clone5 = user.clone();
engine.register_fn("GET_FORM", move |form_name: &str| -> Map {
trace!(
"GET_FORM called for '{}' by user {}",
form_name,
user_clone5.user_id
);
let mut result = Map::new();
result.insert("form_name".into(), Dynamic::from(form_name.to_string()));
result.insert("submitted".into(), Dynamic::from(false));
result
});
// Register VALIDATE_FORM helper
let user_clone6 = user.clone();
engine.register_fn("VALIDATE_FORM", move |form_data: Map| -> Map {
trace!("VALIDATE_FORM called by user {}", user_clone6.user_id);
validate_form_data(&form_data)
});
// Register VALIDATE_FORM with rules
let user_clone7 = user.clone();
engine.register_fn("VALIDATE_FORM", move |form_data: Map, rules: Map| -> Map {
trace!(
"VALIDATE_FORM with rules called by user {}",
user_clone7.user_id
);
validate_form_with_rules(&form_data, &rules)
});
// Register REGISTER_FORM_HANDLER to set up form handler
let state_for_handler = state_clone.clone();
let user_clone8 = user.clone();
engine.register_fn(
"REGISTER_FORM_HANDLER",
move |form_name: &str, handler_script: &str| -> bool {
trace!(
"REGISTER_FORM_HANDLER called for '{}' by user {}",
form_name,
user_clone8.user_id
);
// TODO: Store handler registration in state
info!(
"Registered form handler for '{}' -> '{}'",
form_name, handler_script
);
true
},
);
// Register IS_FORM_SUBMISSION check
let user_clone9 = user.clone();
engine.register_fn("IS_FORM_SUBMISSION", move || -> bool {
trace!("IS_FORM_SUBMISSION called by user {}", user_clone9.user_id);
// This would be set to true when script is invoked from form submission
false
});
// Register GET_SUBMISSION_ID
let user_clone10 = user.clone();
engine.register_fn("GET_SUBMISSION_ID", move || -> String {
trace!("GET_SUBMISSION_ID called by user {}", user_clone10.user_id);
// Generate or return the current submission ID
generate_submission_id()
});
// Register SAVE_SUBMISSION to persist form data
let user_clone11 = user.clone();
engine.register_fn(
"SAVE_SUBMISSION",
move |form_name: &str, data: Map| -> Map {
trace!(
"SAVE_SUBMISSION called for '{}' by user {}",
form_name,
user_clone11.user_id
);
save_form_submission(form_name, &data)
},
);
// Register GET_SUBMISSIONS to retrieve past submissions
let user_clone12 = user.clone();
engine.register_fn("GET_SUBMISSIONS", move |form_name: &str| -> rhai::Array {
trace!(
"GET_SUBMISSIONS called for '{}' by user {}",
form_name,
user_clone12.user_id
);
// TODO: Implement database lookup
rhai::Array::new()
});
// Register GET_SUBMISSIONS with limit
let user_clone13 = user.clone();
engine.register_fn(
"GET_SUBMISSIONS",
move |form_name: &str, limit: i64| -> rhai::Array {
trace!(
"GET_SUBMISSIONS called for '{}' with limit {} by user {}",
form_name,
limit,
user_clone13.user_id
);
// TODO: Implement database lookup with limit
rhai::Array::new()
},
);
debug!("Registered ON FORM SUBMIT keyword and helpers");
}
/// Validate form data with basic rules
fn validate_form_data(form_data: &Map) -> Map {
let mut result = Map::new();
let mut is_valid = true;
let mut errors = rhai::Array::new();
// Check for empty required fields (fields that exist but are empty)
for (key, value) in form_data.iter() {
if value.is_unit() || value.to_string().trim().is_empty() {
// Field is empty - might be an error depending on context
// For basic validation, we just note it
}
}
result.insert("valid".into(), Dynamic::from(is_valid));
result.insert("errors".into(), Dynamic::from(errors));
result.insert("field_count".into(), Dynamic::from(form_data.len() as i64));
result
}
/// Validate form data with custom rules
fn validate_form_with_rules(form_data: &Map, rules: &Map) -> Map {
let mut result = Map::new();
let mut is_valid = true;
let mut errors = rhai::Array::new();
for (field_name, rule) in rules.iter() {
let field_key = field_name.as_str();
let rule_str = rule.to_string().to_lowercase();
// Check if field exists
let field_value = form_data.get(field_key);
if rule_str.contains("required") {
match field_value {
None => {
is_valid = false;
let mut error = Map::new();
error.insert("field".into(), Dynamic::from(field_key.to_string()));
error.insert("rule".into(), Dynamic::from("required"));
error.insert(
"message".into(),
Dynamic::from(format!("Field '{}' is required", field_key)),
);
errors.push(Dynamic::from(error));
}
Some(val) if val.is_unit() || val.to_string().trim().is_empty() => {
is_valid = false;
let mut error = Map::new();
error.insert("field".into(), Dynamic::from(field_key.to_string()));
error.insert("rule".into(), Dynamic::from("required"));
error.insert(
"message".into(),
Dynamic::from(format!("Field '{}' cannot be empty", field_key)),
);
errors.push(Dynamic::from(error));
}
_ => {}
}
}
if rule_str.contains("email") {
if let Some(val) = field_value {
let email = val.to_string();
if !email.is_empty() && !is_valid_email(&email) {
is_valid = false;
let mut error = Map::new();
error.insert("field".into(), Dynamic::from(field_key.to_string()));
error.insert("rule".into(), Dynamic::from("email"));
error.insert(
"message".into(),
Dynamic::from(format!("Field '{}' must be a valid email", field_key)),
);
errors.push(Dynamic::from(error));
}
}
}
if rule_str.contains("phone") {
if let Some(val) = field_value {
let phone = val.to_string();
if !phone.is_empty() && !is_valid_phone(&phone) {
is_valid = false;
let mut error = Map::new();
error.insert("field".into(), Dynamic::from(field_key.to_string()));
error.insert("rule".into(), Dynamic::from("phone"));
error.insert(
"message".into(),
Dynamic::from(format!(
"Field '{}' must be a valid phone number",
field_key
)),
);
errors.push(Dynamic::from(error));
}
}
}
}
result.insert("valid".into(), Dynamic::from(is_valid));
result.insert("errors".into(), Dynamic::from(errors));
result.insert("field_count".into(), Dynamic::from(form_data.len() as i64));
result.insert("rules_checked".into(), Dynamic::from(rules.len() as i64));
result
}
/// Basic email validation
fn is_valid_email(email: &str) -> bool {
let email = email.trim();
if email.is_empty() {
return false;
}
// Simple validation: must contain @ and have something before and after
let parts: Vec<&str> = email.split('@').collect();
if parts.len() != 2 {
return false;
}
let local = parts[0];
let domain = parts[1];
// Local part must not be empty
if local.is_empty() {
return false;
}
// Domain must contain at least one dot and not be empty
if domain.is_empty() || !domain.contains('.') {
return false;
}
// Domain must have something after the last dot
let domain_parts: Vec<&str> = domain.split('.').collect();
if domain_parts.last().map(|s| s.is_empty()).unwrap_or(true) {
return false;
}
true
}
/// Basic phone validation
fn is_valid_phone(phone: &str) -> bool {
let phone = phone.trim();
if phone.is_empty() {
return false;
}
// Remove common formatting characters
let digits: String = phone
.chars()
.filter(|c| c.is_ascii_digit() || *c == '+')
.collect();
// Must have at least 7 digits (minimum for a phone number)
let digit_count = digits.chars().filter(|c| c.is_ascii_digit()).count();
digit_count >= 7
}
/// Generate a unique submission ID
fn generate_submission_id() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis();
format!("sub_{}", timestamp)
}
/// Save form submission to storage
fn save_form_submission(form_name: &str, data: &Map) -> Map {
let mut result = Map::new();
let submission_id = generate_submission_id();
// TODO: Implement actual database storage
info!(
"Saving form submission for '{}' with id '{}'",
form_name, submission_id
);
result.insert("success".into(), Dynamic::from(true));
result.insert("submission_id".into(), Dynamic::from(submission_id));
result.insert("form_name".into(), Dynamic::from(form_name.to_string()));
result.insert("field_count".into(), Dynamic::from(data.len() as i64));
result.insert("timestamp".into(), Dynamic::from(chrono_timestamp()));
result
}
/// Get current timestamp in ISO format
fn chrono_timestamp() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = duration.as_secs();
// Simple ISO-like format without external dependencies
format!("{}Z", secs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_valid_email() {
assert!(is_valid_email("user@example.com"));
assert!(is_valid_email("user.name@example.co.uk"));
assert!(is_valid_email("user+tag@example.com"));
assert!(!is_valid_email("invalid"));
assert!(!is_valid_email("@example.com"));
assert!(!is_valid_email("user@"));
assert!(!is_valid_email("user@example"));
assert!(!is_valid_email(""));
}
#[test]
fn test_is_valid_phone() {
assert!(is_valid_phone("+1234567890"));
assert!(is_valid_phone("123-456-7890"));
assert!(is_valid_phone("(123) 456-7890"));
assert!(is_valid_phone("1234567"));
assert!(!is_valid_phone("123"));
assert!(!is_valid_phone(""));
assert!(!is_valid_phone("abc"));
}
#[test]
fn test_validate_form_data() {
let mut form_data = Map::new();
form_data.insert("name".into(), Dynamic::from("John"));
form_data.insert("email".into(), Dynamic::from("john@example.com"));
let result = validate_form_data(&form_data);
assert!(result.get("valid").unwrap().as_bool().unwrap());
}
#[test]
fn test_validate_form_with_rules_required() {
let mut form_data = Map::new();
form_data.insert("name".into(), Dynamic::from("John"));
// Missing email field
let mut rules = Map::new();
rules.insert("name".into(), Dynamic::from("required"));
rules.insert("email".into(), Dynamic::from("required"));
let result = validate_form_with_rules(&form_data, &rules);
assert!(!result.get("valid").unwrap().as_bool().unwrap());
}
#[test]
fn test_validate_form_with_rules_email() {
let mut form_data = Map::new();
form_data.insert("email".into(), Dynamic::from("invalid-email"));
let mut rules = Map::new();
rules.insert("email".into(), Dynamic::from("email"));
let result = validate_form_with_rules(&form_data, &rules);
assert!(!result.get("valid").unwrap().as_bool().unwrap());
}
#[test]
fn test_generate_submission_id() {
let id = generate_submission_id();
assert!(id.starts_with("sub_"));
}
#[test]
fn test_save_form_submission() {
let mut data = Map::new();
data.insert("name".into(), Dynamic::from("Test"));
let result = save_form_submission("test_form", &data);
assert!(result.get("success").unwrap().as_bool().unwrap());
assert!(result.contains_key("submission_id"));
}
}

View file

@ -0,0 +1,124 @@
//! SEND TEMPLATE Keywords - Wrapper module
//!
//! This module serves as a wrapper for the messaging template functionality,
//! re-exporting the functions from the messaging module for backward compatibility.
//!
//! BASIC Keywords provided:
//! - SEND_TEMPLATE - Send a templated message to a single recipient
//! - SEND_TEMPLATE_TO - Send templated messages to multiple recipients (bulk)
//! - CREATE_TEMPLATE - Create or update a message template
//! - GET_TEMPLATE - Retrieve a message template
//! - LIST_TEMPLATES - List all available templates
//!
//! Supported Channels:
//! - email - Email messages
//! - whatsapp - WhatsApp messages
//! - sms - SMS text messages
//! - telegram - Telegram messages
//! - push - Push notifications
//!
//! Examples:
//! ' Send a single templated email
//! result = SEND_TEMPLATE("welcome", "user@example.com", "email")
//!
//! ' Send with variables
//! vars = #{"name": "John", "order_id": "12345"}
//! result = SEND_TEMPLATE("order_confirmation", "user@example.com", "email", vars)
//!
//! ' Send to multiple recipients
//! recipients = ["user1@example.com", "user2@example.com", "user3@example.com"]
//! result = SEND_TEMPLATE_TO("newsletter", recipients, "email")
//!
//! ' Create a new template
//! CREATE_TEMPLATE "welcome", "email", "Welcome!", "Hello {{name}}, welcome to our service!"
//!
//! ' Retrieve a template
//! template = GET_TEMPLATE("welcome")
//! TALK "Template content: " + template.content
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::Engine;
use std::sync::Arc;
use super::messaging::register_messaging_keywords;
/// Register all send template keywords
///
/// This function delegates to the messaging module's registration function,
/// providing a convenient alias for backward compatibility and clearer intent.
///
/// ## Keywords Registered
///
/// ### SEND_TEMPLATE
/// Send a templated message to a single recipient.
/// ```basic
/// result = SEND_TEMPLATE("template_name", "recipient", "channel")
/// result = SEND_TEMPLATE("template_name", "recipient", "channel", variables)
/// ```
///
/// ### SEND_TEMPLATE_TO
/// Send a templated message to multiple recipients (bulk sending).
/// ```basic
/// result = SEND_TEMPLATE_TO("template_name", recipients_array, "channel")
/// result = SEND_TEMPLATE_TO("template_name", recipients_array, "channel", variables)
/// ```
///
/// ### CREATE_TEMPLATE
/// Create or update a message template.
/// ```basic
/// CREATE_TEMPLATE "name", "channel", "content"
/// CREATE_TEMPLATE "name", "channel", "subject", "content" ' For email with subject
/// ```
///
/// ### GET_TEMPLATE
/// Retrieve a message template by name.
/// ```basic
/// template = GET_TEMPLATE("template_name")
/// template = GET_TEMPLATE("template_name", "channel")
/// ```
///
/// ### LIST_TEMPLATES
/// List all available templates.
/// ```basic
/// templates = LIST_TEMPLATES()
/// FOR EACH t IN templates
/// TALK "Template: " + t
/// NEXT
/// ```
///
/// ## Template Variables
///
/// Templates support variable substitution using double curly braces:
/// ```
/// Hello {{name}}, your order {{order_id}} is ready!
/// ```
///
/// Variables are passed as a map:
/// ```basic
/// vars = #{"name": "John", "order_id": "12345"}
/// SEND_TEMPLATE "order_ready", "user@example.com", "email", vars
/// ```
pub fn register_send_template_keywords(
state: Arc<AppState>,
user: UserSession,
engine: &mut Engine,
) {
debug!("Registering send template keywords...");
// Delegate to messaging module which contains the actual implementation
register_messaging_keywords(state, user, engine);
debug!("Send template keywords registered successfully");
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_structure() {
// This test verifies the module compiles correctly
// Actual function tests are in the messaging/send_template.rs module
assert!(true);
}
}

View file

@ -0,0 +1,116 @@
//! Social Media Keywords - Wrapper module
//!
//! This module serves as a wrapper for social media functionality,
//! re-exporting the functions from the social module for backward compatibility.
//!
//! BASIC Keywords provided:
//! - POST TO - Post content to social media platforms
//! - POST TO AT - Schedule posts for later
//! - GET METRICS - Retrieve engagement metrics
//! - GET POSTS - List posts from a platform
//! - DELETE POST - Remove a post
//!
//! Supported Platforms:
//! - Instagram (via Graph API)
//! - Facebook (via Graph API)
//! - LinkedIn (via LinkedIn API)
//! - Twitter/X (via Twitter API v2)
//!
//! Examples:
//! ' Post to Instagram
//! POST TO "instagram" WITH
//! image = "https://example.com/image.jpg"
//! caption = "Check out our new product! #launch"
//! END WITH
//!
//! ' Schedule a post for later
//! POST TO "facebook" AT "2024-12-25 09:00:00" WITH
//! text = "Merry Christmas from our team!"
//! END WITH
//!
//! ' Get engagement metrics
//! metrics = GET METRICS FROM "instagram" FOR post_id
//! TALK "Likes: " + metrics.likes + ", Comments: " + metrics.comments
//!
//! ' Get recent posts
//! posts = GET POSTS FROM "twitter" LIMIT 10
//! FOR EACH post IN posts
//! TALK post.text + " - " + post.likes + " likes"
//! NEXT
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::Engine;
use std::sync::Arc;
use super::social::register_social_media_keywords as register_social_keywords_impl;
/// Register all social media keywords
///
/// This function delegates to the social module's registration function,
/// providing a convenient alias for backward compatibility and clearer intent.
///
/// ## Keywords Registered
///
/// ### POST TO
/// Post content to a social media platform.
/// ```basic
/// POST TO "instagram" WITH
/// image = "path/to/image.jpg"
/// caption = "My post caption #hashtag"
/// END WITH
/// ```
///
/// ### POST TO AT (Scheduled Posting)
/// Schedule a post for a specific time.
/// ```basic
/// POST TO "facebook" AT DATEADD(NOW(), 1, "hour") WITH
/// text = "This will be posted in 1 hour!"
/// END WITH
/// ```
///
/// ### GET METRICS
/// Retrieve engagement metrics for a post.
/// ```basic
/// metrics = GET_INSTAGRAM_METRICS(post_id)
/// metrics = GET_FACEBOOK_METRICS(post_id)
/// metrics = GET_LINKEDIN_METRICS(post_id)
/// metrics = GET_TWITTER_METRICS(post_id)
/// ```
///
/// ### GET POSTS
/// List posts from a platform.
/// ```basic
/// posts = GET_INSTAGRAM_POSTS(10) ' Get last 10 posts
/// posts = GET_FACEBOOK_POSTS(20)
/// ```
///
/// ### DELETE POST
/// Remove a post from a platform.
/// ```basic
/// DELETE_INSTAGRAM_POST(post_id)
/// DELETE_FACEBOOK_POST(post_id)
/// ```
pub fn register_social_media_keywords(
state: Arc<AppState>,
user: UserSession,
engine: &mut Engine,
) {
debug!("Registering social media keywords...");
// Delegate to social module which contains the actual implementation
register_social_keywords_impl(state, user, engine);
debug!("Social media keywords registered successfully");
}
#[cfg(test)]
mod tests {
#[test]
fn test_module_structure() {
// This test verifies the module compiles correctly
// Actual function tests are in the social/ module files
assert!(true);
}
}

View file

@ -0,0 +1,141 @@
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine};
use std::sync::Arc;
/// Registers the ISEMPTY function for checking if a value is empty
///
/// BASIC Syntax:
/// result = ISEMPTY(value)
///
/// Returns TRUE if value is:
/// - An empty string ""
/// - An empty array []
/// - An empty map {}
/// - Unit/null type
///
/// Examples:
/// IF ISEMPTY(name) THEN
/// TALK "Please provide your name"
/// END IF
///
/// empty_check = ISEMPTY("") ' Returns TRUE
/// empty_check = ISEMPTY("hello") ' Returns FALSE
/// empty_check = ISEMPTY([]) ' Returns TRUE
pub fn isempty_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// ISEMPTY - uppercase version
engine.register_fn("ISEMPTY", |value: Dynamic| -> bool {
check_empty(&value)
});
// isempty - lowercase version
engine.register_fn("isempty", |value: Dynamic| -> bool {
check_empty(&value)
});
// IsEmpty - mixed case version
engine.register_fn("IsEmpty", |value: Dynamic| -> bool {
check_empty(&value)
});
debug!("Registered ISEMPTY keyword");
}
/// Helper function to check if a Dynamic value is empty
fn check_empty(value: &Dynamic) -> bool {
// Check for unit/null type
if value.is_unit() {
return true;
}
// Check for empty string
if value.is_string() {
if let Some(s) = value.clone().into_string().ok() {
return s.is_empty();
}
}
// Check for empty array
if value.is_array() {
if let Ok(arr) = value.clone().into_array() {
return arr.is_empty();
}
}
// Check for empty map
if value.is_map() {
if let Ok(map) = value.as_map() {
return map.is_empty();
}
}
// Check for special "empty" boolean state
if value.is_bool() {
// Boolean false is not considered "empty" - it's a valid value
return false;
}
// Numbers are never empty
if value.is_int() || value.is_float() {
return false;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use rhai::{Array, Map};
#[test]
fn test_empty_string() {
let value = Dynamic::from("");
assert!(check_empty(&value));
}
#[test]
fn test_non_empty_string() {
let value = Dynamic::from("hello");
assert!(!check_empty(&value));
}
#[test]
fn test_empty_array() {
let value = Dynamic::from(Array::new());
assert!(check_empty(&value));
}
#[test]
fn test_non_empty_array() {
let mut arr = Array::new();
arr.push(Dynamic::from(1));
let value = Dynamic::from(arr);
assert!(!check_empty(&value));
}
#[test]
fn test_empty_map() {
let value = Dynamic::from(Map::new());
assert!(check_empty(&value));
}
#[test]
fn test_unit() {
let value = Dynamic::UNIT;
assert!(check_empty(&value));
}
#[test]
fn test_number_not_empty() {
let value = Dynamic::from(0);
assert!(!check_empty(&value));
}
#[test]
fn test_bool_not_empty() {
let value = Dynamic::from(false);
assert!(!check_empty(&value));
}
}

View file

@ -0,0 +1,41 @@
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine};
use std::sync::Arc;
pub fn isnull_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// ISNULL - Check if value is null/unit
engine.register_fn("ISNULL", |value: Dynamic| -> bool { value.is_unit() });
engine.register_fn("isnull", |value: Dynamic| -> bool { value.is_unit() });
// IsNull - case variation
engine.register_fn("IsNull", |value: Dynamic| -> bool { value.is_unit() });
debug!("Registered ISNULL keyword");
}
#[cfg(test)]
mod tests {
#[test]
fn test_isnull_unit() {
use rhai::Dynamic;
let value = Dynamic::UNIT;
assert!(value.is_unit());
}
#[test]
fn test_isnull_not_unit() {
use rhai::Dynamic;
let value = Dynamic::from("test");
assert!(!value.is_unit());
}
#[test]
fn test_isnull_number() {
use rhai::Dynamic;
let value = Dynamic::from(42);
assert!(!value.is_unit());
}
}

View file

@ -0,0 +1,30 @@
pub mod isempty;
pub mod isnull;
pub mod str_val;
pub mod typeof_check;
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::Engine;
use std::sync::Arc;
pub fn register_validation_functions(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
str_val::val_keyword(&state, user.clone(), engine);
str_val::str_keyword(&state, user.clone(), engine);
str_val::cint_keyword(&state, user.clone(), engine);
str_val::cdbl_keyword(&state, user.clone(), engine);
isnull::isnull_keyword(&state, user.clone(), engine);
isempty::isempty_keyword(&state, user.clone(), engine);
typeof_check::typeof_keyword(&state, user.clone(), engine);
typeof_check::isarray_keyword(&state, user.clone(), engine);
typeof_check::isnumber_keyword(&state, user.clone(), engine);
typeof_check::isstring_keyword(&state, user.clone(), engine);
typeof_check::isbool_keyword(&state, user.clone(), engine);
nvl_iif::nvl_keyword(&state, user.clone(), engine);
nvl_iif::iif_keyword(&state, user, engine);
debug!("Registered all validation functions");
}
pub mod nvl_iif;

View file

@ -0,0 +1,181 @@
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine};
use std::sync::Arc;
/// NVL - Returns the first non-null value (coalesce)
/// Syntax: NVL(value, default)
pub fn nvl_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// NVL with two arguments
engine.register_fn("NVL", |value: Dynamic, default: Dynamic| -> Dynamic {
if value.is_unit() || value.to_string().is_empty() {
default
} else {
value
}
});
engine.register_fn("nvl", |value: Dynamic, default: Dynamic| -> Dynamic {
if value.is_unit() || value.to_string().is_empty() {
default
} else {
value
}
});
// COALESCE alias for NVL
engine.register_fn("COALESCE", |value: Dynamic, default: Dynamic| -> Dynamic {
if value.is_unit() || value.to_string().is_empty() {
default
} else {
value
}
});
engine.register_fn("coalesce", |value: Dynamic, default: Dynamic| -> Dynamic {
if value.is_unit() || value.to_string().is_empty() {
default
} else {
value
}
});
// IFNULL alias
engine.register_fn("IFNULL", |value: Dynamic, default: Dynamic| -> Dynamic {
if value.is_unit() || value.to_string().is_empty() {
default
} else {
value
}
});
engine.register_fn("ifnull", |value: Dynamic, default: Dynamic| -> Dynamic {
if value.is_unit() || value.to_string().is_empty() {
default
} else {
value
}
});
debug!("Registered NVL/COALESCE/IFNULL keywords");
}
/// IIF - Immediate If (ternary operator)
/// Syntax: IIF(condition, true_value, false_value)
pub fn iif_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
// IIF with boolean condition
engine.register_fn(
"IIF",
|condition: bool, true_val: Dynamic, false_val: Dynamic| -> Dynamic {
if condition {
true_val
} else {
false_val
}
},
);
engine.register_fn(
"iif",
|condition: bool, true_val: Dynamic, false_val: Dynamic| -> Dynamic {
if condition {
true_val
} else {
false_val
}
},
);
// IF alias (common in many BASIC dialects)
engine.register_fn(
"IF_FUNC",
|condition: bool, true_val: Dynamic, false_val: Dynamic| -> Dynamic {
if condition {
true_val
} else {
false_val
}
},
);
// CHOOSE - select from list based on index (1-based)
engine.register_fn(
"CHOOSE",
|index: i64, val1: Dynamic, val2: Dynamic| -> Dynamic {
match index {
1 => val1,
2 => val2,
_ => Dynamic::UNIT,
}
},
);
engine.register_fn(
"choose",
|index: i64, val1: Dynamic, val2: Dynamic| -> Dynamic {
match index {
1 => val1,
2 => val2,
_ => Dynamic::UNIT,
}
},
);
// SWITCH function - evaluate expression and return matching result
// SWITCH(expr, val1, result1, val2, result2, ..., default)
// This is a simplified 2-case version
engine.register_fn(
"SWITCH_FUNC",
|expr: Dynamic, val1: Dynamic, result1: Dynamic, default: Dynamic| -> Dynamic {
if expr.to_string() == val1.to_string() {
result1
} else {
default
}
},
);
debug!("Registered IIF/IF_FUNC/CHOOSE/SWITCH_FUNC keywords");
}
#[cfg(test)]
mod tests {
#[test]
fn test_nvl_logic() {
let value = "";
let default = "default";
let result = if value.is_empty() { default } else { value };
assert_eq!(result, "default");
}
#[test]
fn test_nvl_with_value() {
let value = "actual";
let default = "default";
let result = if value.is_empty() { default } else { value };
assert_eq!(result, "actual");
}
#[test]
fn test_iif_true() {
let condition = true;
let result = if condition { "yes" } else { "no" };
assert_eq!(result, "yes");
}
#[test]
fn test_iif_false() {
let condition = false;
let result = if condition { "yes" } else { "no" };
assert_eq!(result, "no");
}
#[test]
fn test_choose() {
let index = 2;
let values = vec!["first", "second", "third"];
let result = values.get((index - 1) as usize).unwrap_or(&"");
assert_eq!(*result, "second");
}
}

View file

@ -0,0 +1,149 @@
//! Type conversion functions: VAL, STR, CINT, CDBL
//!
//! These functions convert between string and numeric types,
//! following classic BASIC conventions.
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine};
use std::sync::Arc;
/// VAL - Convert string to number (float)
/// Returns 0.0 if conversion fails
pub fn val_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("VAL", |s: &str| -> f64 {
s.trim().parse::<f64>().unwrap_or(0.0)
});
engine.register_fn("val", |s: &str| -> f64 {
s.trim().parse::<f64>().unwrap_or(0.0)
});
// Also handle Dynamic input
engine.register_fn("VAL", |v: Dynamic| -> f64 {
if v.is_int() {
return v.as_int().unwrap_or(0) as f64;
}
if v.is_float() {
return v.as_float().unwrap_or(0.0);
}
v.to_string().trim().parse::<f64>().unwrap_or(0.0)
});
debug!("Registered VAL keyword");
}
/// STR - Convert number to string
pub fn str_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("STR", |n: i64| -> String { n.to_string() });
engine.register_fn("str", |n: i64| -> String { n.to_string() });
engine.register_fn("STR", |n: f64| -> String {
// Remove trailing zeros for cleaner output
let s = format!("{}", n);
s
});
engine.register_fn("str", |n: f64| -> String { format!("{}", n) });
// Handle Dynamic input
engine.register_fn("STR", |v: Dynamic| -> String { v.to_string() });
engine.register_fn("str", |v: Dynamic| -> String { v.to_string() });
debug!("Registered STR keyword");
}
/// CINT - Convert to integer (rounds to nearest integer)
pub fn cint_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("CINT", |n: f64| -> i64 { n.round() as i64 });
engine.register_fn("cint", |n: f64| -> i64 { n.round() as i64 });
engine.register_fn("CINT", |n: i64| -> i64 { n });
engine.register_fn("cint", |n: i64| -> i64 { n });
engine.register_fn("CINT", |s: &str| -> i64 {
s.trim()
.parse::<f64>()
.map(|f| f.round() as i64)
.unwrap_or(0)
});
engine.register_fn("cint", |s: &str| -> i64 {
s.trim()
.parse::<f64>()
.map(|f| f.round() as i64)
.unwrap_or(0)
});
// Handle Dynamic
engine.register_fn("CINT", |v: Dynamic| -> i64 {
if v.is_int() {
return v.as_int().unwrap_or(0);
}
if v.is_float() {
return v.as_float().unwrap_or(0.0).round() as i64;
}
v.to_string()
.trim()
.parse::<f64>()
.map(|f| f.round() as i64)
.unwrap_or(0)
});
debug!("Registered CINT keyword");
}
/// CDBL - Convert to double (floating point)
pub fn cdbl_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("CDBL", |n: i64| -> f64 { n as f64 });
engine.register_fn("cdbl", |n: i64| -> f64 { n as f64 });
engine.register_fn("CDBL", |n: f64| -> f64 { n });
engine.register_fn("cdbl", |n: f64| -> f64 { n });
engine.register_fn("CDBL", |s: &str| -> f64 {
s.trim().parse::<f64>().unwrap_or(0.0)
});
engine.register_fn("cdbl", |s: &str| -> f64 {
s.trim().parse::<f64>().unwrap_or(0.0)
});
// Handle Dynamic
engine.register_fn("CDBL", |v: Dynamic| -> f64 {
if v.is_float() {
return v.as_float().unwrap_or(0.0);
}
if v.is_int() {
return v.as_int().unwrap_or(0) as f64;
}
v.to_string().trim().parse::<f64>().unwrap_or(0.0)
});
debug!("Registered CDBL keyword");
}
#[cfg(test)]
mod tests {
#[test]
fn test_val_parsing() {
assert_eq!("123.45".trim().parse::<f64>().unwrap_or(0.0), 123.45);
assert_eq!(" 456 ".trim().parse::<f64>().unwrap_or(0.0), 456.0);
assert_eq!("abc".trim().parse::<f64>().unwrap_or(0.0), 0.0);
}
#[test]
fn test_cint_rounding() {
assert_eq!(2.4_f64.round() as i64, 2);
assert_eq!(2.5_f64.round() as i64, 3);
assert_eq!(2.6_f64.round() as i64, 3);
assert_eq!((-2.5_f64).round() as i64, -3);
}
}

View file

@ -0,0 +1,161 @@
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::debug;
use rhai::{Dynamic, Engine};
use std::sync::Arc;
/// TYPEOF - Returns the type of a value as a string
pub fn typeof_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("TYPEOF", |value: Dynamic| -> String {
get_type_name(&value)
});
engine.register_fn("typeof", |value: Dynamic| -> String {
get_type_name(&value)
});
engine.register_fn("TYPENAME", |value: Dynamic| -> String {
get_type_name(&value)
});
debug!("Registered TYPEOF keyword");
}
/// ISARRAY - Check if value is an array
pub fn isarray_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("ISARRAY", |value: Dynamic| -> bool {
value.is_array()
});
engine.register_fn("isarray", |value: Dynamic| -> bool {
value.is_array()
});
engine.register_fn("IS_ARRAY", |value: Dynamic| -> bool {
value.is_array()
});
debug!("Registered ISARRAY keyword");
}
/// ISNUMBER - Check if value is a number (int or float)
pub fn isnumber_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("ISNUMBER", |value: Dynamic| -> bool {
is_numeric(&value)
});
engine.register_fn("isnumber", |value: Dynamic| -> bool {
is_numeric(&value)
});
engine.register_fn("IS_NUMBER", |value: Dynamic| -> bool {
is_numeric(&value)
});
engine.register_fn("ISNUMERIC", |value: Dynamic| -> bool {
is_numeric(&value)
});
debug!("Registered ISNUMBER keyword");
}
/// ISSTRING - Check if value is a string
pub fn isstring_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("ISSTRING", |value: Dynamic| -> bool {
value.is_string()
});
engine.register_fn("isstring", |value: Dynamic| -> bool {
value.is_string()
});
engine.register_fn("IS_STRING", |value: Dynamic| -> bool {
value.is_string()
});
debug!("Registered ISSTRING keyword");
}
/// ISBOOL - Check if value is a boolean
pub fn isbool_keyword(_state: &Arc<AppState>, _user: UserSession, engine: &mut Engine) {
engine.register_fn("ISBOOL", |value: Dynamic| -> bool {
value.is_bool()
});
engine.register_fn("isbool", |value: Dynamic| -> bool {
value.is_bool()
});
engine.register_fn("IS_BOOL", |value: Dynamic| -> bool {
value.is_bool()
});
engine.register_fn("ISBOOLEAN", |value: Dynamic| -> bool {
value.is_bool()
});
debug!("Registered ISBOOL keyword");
}
/// Helper function to get the type name of a Dynamic value
fn get_type_name(value: &Dynamic) -> String {
if value.is_unit() {
"null".to_string()
} else if value.is_bool() {
"boolean".to_string()
} else if value.is_int() {
"integer".to_string()
} else if value.is_float() {
"float".to_string()
} else if value.is_string() {
"string".to_string()
} else if value.is_array() {
"array".to_string()
} else if value.is_map() {
"object".to_string()
} else if value.is_char() {
"char".to_string()
} else {
value.type_name().to_string()
}
}
/// Helper function to check if a value is numeric
fn is_numeric(value: &Dynamic) -> bool {
if value.is_int() || value.is_float() {
return true;
}
// Also check if it's a string that can be parsed as a number
if value.is_string() {
if let Ok(s) = value.clone().into_string() {
return s.trim().parse::<f64>().is_ok();
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_type_name() {
assert_eq!(get_type_name(&Dynamic::UNIT), "null");
assert_eq!(get_type_name(&Dynamic::from(true)), "boolean");
assert_eq!(get_type_name(&Dynamic::from(42_i64)), "integer");
assert_eq!(get_type_name(&Dynamic::from(3.14_f64)), "float");
assert_eq!(get_type_name(&Dynamic::from("hello")), "string");
}
#[test]
fn test_is_numeric() {
assert!(is_numeric(&Dynamic::from(42_i64)));
assert!(is_numeric(&Dynamic::from(3.14_f64)));
assert!(is_numeric(&Dynamic::from("123")));
assert!(is_numeric(&Dynamic::from("3.14")));
assert!(!is_numeric(&Dynamic::from("hello")));
assert!(!is_numeric(&Dynamic::from(true)));
}
}

View file

@ -1,397 +1,235 @@
REM ============================================================================
REM Lead Nurturing Campaign with AI Scoring
REM General Bots Marketing Automation Template
REM ============================================================================
REM This campaign automatically nurtures leads based on their AI-calculated
REM lead score, sending personalized content at optimal intervals.
REM ============================================================================
' Lead Nurturing Campaign with AI Scoring
' AI-powered lead nurturing with dynamic scoring and personalized messaging
DESCRIPTION "AI-powered lead nurturing campaign with dynamic scoring and personalized messaging"
REM ============================================================================
REM Campaign Configuration
REM ============================================================================
DESCRIPTION "AI-powered lead nurturing campaign with dynamic scoring"
' Campaign Configuration
campaign_name = "lead-nurture-2025"
campaign_duration_days = 30
min_score_threshold = 20
mql_threshold = 70
sql_threshold = 85
REM Email sending intervals (in days)
email_interval_cold = 7
email_interval_warm = 3
email_interval_hot = 1
REM ============================================================================
REM Main Campaign Entry Point
REM ============================================================================
REM This is triggered by ON FORM SUBMIT from landing pages
' Form Submit Handler - triggered by landing page submissions
ON FORM SUBMIT "landing-page"
REM Extract lead data from form submission
lead_email = fields.email
lead_name = NVL(fields.name, "there")
lead_company = fields.company
lead_phone = fields.phone
lead_source = metadata.utm_source
TALK "🎯 New lead captured: " + lead_email
TALK "New lead captured: " + lead_email
REM Create lead profile for scoring
lead = NEW OBJECT
lead.email = lead_email
lead.name = lead_name
lead.company = lead_company
lead.source = lead_source
lead.created_at = NOW()
' Score the lead with AI
WITH lead
.email = lead_email
.name = lead_name
.company = lead_company
.source = lead_source
.created_at = NOW()
END WITH
REM Calculate initial AI lead score
score_result = AI SCORE LEAD lead
TALK "📊 Lead Score: " + score_result.score + " (Grade: " + score_result.grade + ")"
TALK "Lead Score: " + score_result.score + " (Grade: " + score_result.grade + ")"
REM Save lead to CRM
' Save to CRM
SAVE "leads", lead_email, lead_name, lead_company, lead_phone, lead_source, score_result.score, score_result.grade, NOW()
REM Determine nurture track based on score
' Route based on score
IF score_result.score >= sql_threshold THEN
REM Hot lead - immediate sales handoff
CALL hot_lead_workflow(lead, score_result)
ELSE IF score_result.score >= mql_threshold THEN
REM Warm lead - accelerated nurture
CALL warm_lead_workflow(lead, score_result)
ELSE IF score_result.score >= min_score_threshold THEN
REM Cold lead - standard nurture
CALL cold_lead_workflow(lead, score_result)
ELSE
REM Very cold - add to long-term drip
CALL drip_campaign_workflow(lead, score_result)
CALL cold_lead_workflow(lead, score_result)
END IF
END ON
REM ============================================================================
REM Hot Lead Workflow (Score >= 85)
REM ============================================================================
' Hot Lead Workflow (Score >= 85)
SUB hot_lead_workflow(lead, score_result)
TALK "🔥 HOT LEAD: " + lead.email + " - Initiating sales handoff"
TALK "HOT LEAD: " + lead.email + " - Sales handoff"
REM Send immediate welcome + calendar booking
vars = NEW OBJECT
vars.name = lead.name
vars.score = score_result.score
vars.company = NVL(lead.company, "your company")
WITH vars
.name = lead.name
.score = score_result.score
.company = NVL(lead.company, "your company")
END WITH
REM Send personalized welcome via multiple channels
SEND TEMPLATE "hot-lead-welcome", "email", lead.email, vars
IF NOT ISEMPTY(lead.phone) THEN
SEND TEMPLATE "hot-lead-sms", "sms", lead.phone, vars
END IF
REM Create task for sales team
CREATE TASK "Contact hot lead: " + lead.email, "sales-team", "high", NOW()
REM Send Slack notification to sales
sales_alert = "🔥 *HOT LEAD ALERT*\n"
sales_alert = sales_alert + "Email: " + lead.email + "\n"
sales_alert = sales_alert + "Score: " + score_result.score + "\n"
sales_alert = sales_alert + "Company: " + NVL(lead.company, "Unknown") + "\n"
sales_alert = sales_alert + "Action: Immediate follow-up required!"
' Slack notification
WITH alert
.text = "HOT LEAD: " + lead.email + " | Score: " + score_result.score
END WITH
POST "https://hooks.slack.com/services/YOUR_WEBHOOK", alert
POST "https://hooks.slack.com/services/YOUR_WEBHOOK", #{text: sales_alert}
SET SCHEDULE DATEADD(NOW(), 1, "day"), "hot-lead-followup.bas"
REM Schedule follow-up if no response in 24 hours
SET SCHEDULE "0 9 * * *", "hot-lead-followup.bas"
TALK "✅ Hot lead workflow completed for " + lead.email
TALK "Hot lead workflow completed"
END SUB
REM ============================================================================
REM Warm Lead Workflow (Score 70-84)
REM ============================================================================
' Warm Lead Workflow (Score 70-84)
SUB warm_lead_workflow(lead, score_result)
TALK "🌡️ WARM LEAD: " + lead.email + " - Starting accelerated nurture"
TALK "WARM LEAD: " + lead.email + " - Accelerated nurture"
vars = NEW OBJECT
vars.name = lead.name
vars.company = NVL(lead.company, "your company")
WITH vars
.name = lead.name
.company = NVL(lead.company, "your company")
END WITH
REM Day 0: Welcome email with case study
SEND TEMPLATE "warm-welcome", "email", lead.email, vars
REM Schedule Day 3: Value proposition email
nurture_data = #{
lead_email: lead.email,
lead_name: lead.name,
template: "warm-value-prop",
step: 2
}
SET SCHEDULE DATEADD(NOW(), 3, "day"), "send-nurture-email.bas"
REM Schedule Day 7: Demo invitation
SET SCHEDULE DATEADD(NOW(), 3, "day"), "warm-nurture-2.bas"
SET SCHEDULE DATEADD(NOW(), 7, "day"), "warm-demo-invite.bas"
REM Schedule Day 14: Re-score and evaluate
SET SCHEDULE DATEADD(NOW(), 14, "day"), "rescore-lead.bas"
TALK "✅ Warm lead nurture sequence started for " + lead.email
TALK "Warm lead nurture started"
END SUB
REM ============================================================================
REM Cold Lead Workflow (Score 20-69)
REM ============================================================================
' Cold Lead Workflow (Score < 70)
SUB cold_lead_workflow(lead, score_result)
TALK "❄️ COLD LEAD: " + lead.email + " - Starting standard nurture"
TALK "COLD LEAD: " + lead.email + " - Standard nurture"
vars = NEW OBJECT
vars.name = lead.name
vars.company = NVL(lead.company, "your organization")
WITH vars
.name = lead.name
.company = NVL(lead.company, "your organization")
END WITH
REM Day 0: Welcome email
SEND TEMPLATE "cold-welcome", "email", lead.email, vars
REM Day 7: Educational content
SET SCHEDULE DATEADD(NOW(), 7, "day"), "cold-education-1.bas"
REM Day 14: More educational content
SET SCHEDULE DATEADD(NOW(), 14, "day"), "cold-education-2.bas"
REM Day 21: Soft pitch
SET SCHEDULE DATEADD(NOW(), 21, "day"), "cold-soft-pitch.bas"
REM Day 30: Re-score and decide next steps
SET SCHEDULE DATEADD(NOW(), 30, "day"), "rescore-lead.bas"
TALK "✅ Cold lead nurture sequence started for " + lead.email
TALK "Cold lead nurture started"
END SUB
REM ============================================================================
REM Long-term Drip Campaign (Score < 20)
REM ============================================================================
SUB drip_campaign_workflow(lead, score_result)
TALK "💧 LOW SCORE LEAD: " + lead.email + " - Adding to drip campaign"
vars = NEW OBJECT
vars.name = lead.name
REM Simple welcome only
SEND TEMPLATE "drip-welcome", "email", lead.email, vars
REM Add to monthly newsletter
SAVE "newsletter_subscribers", lead.email, lead.name, NOW()
REM Schedule monthly check-in
SET SCHEDULE "0 10 1 * *", "monthly-drip-check.bas"
TALK "✅ Added " + lead.email + " to long-term drip campaign"
END SUB
REM ============================================================================
REM Scheduled: Re-score Lead and Adjust Campaign
REM ============================================================================
' Re-score Lead (scheduled)
SUB rescore_lead()
PARAM lead_email AS string
REM Get current lead data
lead_data = FIND "leads", "email = '" + lead_email + "'"
IF ISEMPTY(lead_data) THEN
TALK "⚠️ Lead not found: " + lead_email
TALK "Lead not found: " + lead_email
RETURN
END IF
REM Get updated behavior data
lead = NEW OBJECT
lead.email = lead_email
lead.name = lead_data.name
lead.company = lead_data.company
WITH lead
.email = lead_email
.name = lead_data.name
.company = lead_data.company
END WITH
REM Recalculate score with latest behavior
new_score = AI SCORE LEAD lead
old_score = lead_data.score
score_change = new_score.score - old_score
TALK "📊 Lead Rescore: " + lead_email
TALK " Old Score: " + old_score + " → New Score: " + new_score.score
TALK " Change: " + IIF(score_change >= 0, "+", "") + score_change
TALK "Lead Rescore: " + lead_email
TALK "Old: " + old_score + " -> New: " + new_score.score + " (" + IIF(score_change >= 0, "+", "") + score_change + ")"
REM Update stored score
UPDATE "leads", lead_email, #{score: new_score.score, grade: new_score.grade, updated_at: NOW()}
UPDATE "leads", lead_email, new_score.score, new_score.grade, NOW()
REM Check if lead should be promoted to higher tier
IF old_score < mql_threshold AND new_score.score >= mql_threshold THEN
TALK "🎉 Lead promoted to MQL: " + lead_email
TALK "Lead promoted to MQL: " + lead_email
CALL warm_lead_workflow(lead, new_score)
REM Notify marketing team
SEND TEMPLATE "mql-promotion-alert", "email", "marketing@company.com", #{
lead_email: lead_email,
old_score: old_score,
new_score: new_score.score
}
SEND TEMPLATE "mql-promotion-alert", "email", "marketing@company.com", lead
ELSE IF old_score < sql_threshold AND new_score.score >= sql_threshold THEN
TALK "🔥 Lead promoted to SQL: " + lead_email
TALK "Lead promoted to SQL: " + lead_email
CALL hot_lead_workflow(lead, new_score)
ELSE IF score_change < -20 THEN
TALK "⚠️ Significant score drop for: " + lead_email
REM Move to re-engagement campaign
CALL reengagement_workflow(lead, new_score)
END IF
END SUB
REM ============================================================================
REM Re-engagement Workflow for Declining Leads
REM ============================================================================
SUB reengagement_workflow(lead, score_result)
TALK "🔄 Starting re-engagement for: " + lead.email
vars = NEW OBJECT
vars.name = lead.name
REM Send re-engagement email
SEND TEMPLATE "reengagement", "email", lead.email, vars
REM If we have phone, send SMS too
IF NOT ISEMPTY(lead.phone) THEN
SEND TEMPLATE "reengagement-sms", "sms", lead.phone, vars
END IF
REM Schedule unsubscribe if no engagement in 14 days
SET SCHEDULE DATEADD(NOW(), 14, "day"), "check-reengagement.bas"
TALK "✅ Re-engagement campaign started for " + lead.email
END SUB
REM ============================================================================
REM Utility: Send Nurture Email by Step
REM ============================================================================
' Send Nurture Email (utility)
SUB send_nurture_email()
PARAM lead_email AS string
PARAM template_name AS string
PARAM step AS integer
REM Get lead data
lead_data = FIND "leads", "email = '" + lead_email + "'"
IF ISEMPTY(lead_data) THEN
TALK "⚠️ Lead not found, skipping: " + lead_email
RETURN
END IF
REM Check if lead has unsubscribed
unsubscribed = FIND "unsubscribes", "email = '" + lead_email + "'"
IF NOT ISEMPTY(unsubscribed) THEN
TALK "⏹️ Lead unsubscribed, stopping nurture: " + lead_email
TALK "Lead unsubscribed: " + lead_email
RETURN
END IF
REM Check current score - maybe they've become hot
current_score = GET LEAD SCORE lead_email
IF current_score.score >= sql_threshold THEN
TALK "🔥 Lead is now hot! Switching to hot workflow: " + lead_email
lead = #{email: lead_email, name: lead_data.name, company: lead_data.company}
WITH lead
.email = lead_email
.name = lead_data.name
.company = lead_data.company
END WITH
CALL hot_lead_workflow(lead, current_score)
RETURN
END IF
REM Send the scheduled email
vars = NEW OBJECT
vars.name = lead_data.name
vars.company = NVL(lead_data.company, "your organization")
vars.step = step
WITH vars
.name = lead_data.name
.company = NVL(lead_data.company, "your organization")
.step = step
END WITH
result = SEND TEMPLATE template_name, "email", lead_email, vars
IF result[0].success THEN
TALK "✉️ Nurture email sent: " + template_name + " to " + lead_email
REM Track email send
TALK "Nurture email sent: " + template_name + " to " + lead_email
SAVE "email_tracking", lead_email, template_name, step, NOW(), "sent"
REM Update lead score for engagement
UPDATE LEAD SCORE lead_email, 2, "Nurture email " + step + " sent"
ELSE
TALK "Failed to send nurture email: " + result[0].error
TALK "Failed to send: " + result[0].error
END IF
END SUB
REM ============================================================================
REM Campaign Analytics
REM ============================================================================
' Campaign Analytics
SUB get_campaign_analytics()
TALK "📈 Campaign Analytics for: " + campaign_name
TALK "================================"
TALK "Campaign Analytics: " + campaign_name
REM Total leads
total_leads = AGGREGATE "leads", "COUNT", "email"
TALK "Total Leads: " + total_leads
REM Leads by grade
grade_a = AGGREGATE "leads", "COUNT", "email", "grade = 'A'"
grade_b = AGGREGATE "leads", "COUNT", "email", "grade = 'B'"
grade_c = AGGREGATE "leads", "COUNT", "email", "grade = 'C'"
grade_d = AGGREGATE "leads", "COUNT", "email", "grade = 'D'"
TALK "Grade Distribution:"
TALK " A (Hot): " + grade_a
TALK " B (Warm): " + grade_b
TALK " C (Neutral): " + grade_c
TALK " D (Cold): " + grade_d
REM Average score
avg_score = AGGREGATE "leads", "AVG", "score"
TALK "Average Lead Score: " + ROUND(avg_score, 1)
REM Conversion rates
mql_count = AGGREGATE "leads", "COUNT", "email", "score >= " + mql_threshold
sql_count = AGGREGATE "leads", "COUNT", "email", "score >= " + sql_threshold
emails_sent = AGGREGATE "email_tracking", "COUNT", "id", "status = 'sent'"
TALK "Total Leads: " + total_leads
TALK "Grade A: " + grade_a + " | B: " + grade_b + " | C: " + grade_c
TALK "Avg Score: " + ROUND(avg_score, 1)
IF total_leads > 0 THEN
mql_rate = ROUND((mql_count / total_leads) * 100, 1)
sql_rate = ROUND((sql_count / total_leads) * 100, 1)
TALK "Conversion Rates:"
TALK " MQL Rate: " + mql_rate + "%"
TALK " SQL Rate: " + sql_rate + "%"
TALK "MQL Rate: " + mql_rate + "% | SQL Rate: " + sql_rate + "%"
END IF
REM Email performance
emails_sent = AGGREGATE "email_tracking", "COUNT", "id", "status = 'sent'"
TALK "Emails Sent: " + emails_sent
TALK "================================"
WITH stats
.total_leads = total_leads
.grade_a = grade_a
.grade_b = grade_b
.grade_c = grade_c
.avg_score = avg_score
.mql_count = mql_count
.sql_count = sql_count
END WITH
RETURN #{
total_leads: total_leads,
grade_a: grade_a,
grade_b: grade_b,
grade_c: grade_c,
grade_d: grade_d,
avg_score: avg_score,
mql_count: mql_count,
sql_count: sql_count,
emails_sent: emails_sent
}
RETURN stats
END SUB
REM ============================================================================
REM Entry point for manual campaign trigger
REM ============================================================================
TALK "🚀 Lead Nurturing Campaign Ready: " + campaign_name
TALK "📊 MQL Threshold: " + mql_threshold
TALK "🔥 SQL Threshold: " + sql_threshold
TALK "⏰ Campaign Duration: " + campaign_duration_days + " days"
TALK ""
TALK "Waiting for form submissions..."
TALK "Lead Nurturing Campaign Ready: " + campaign_name
TALK "MQL Threshold: " + mql_threshold + " | SQL Threshold: " + sql_threshold

View file

@ -25,4 +25,72 @@ WITH variables
.date = TODAY()
END WITH
result = SEND TEMPLATE "welcome-email-1",
result = SEND TEMPLATE "welcome-email-1", "email", lead_email, variables
IF result[0].success THEN
TALK "Welcome email #1 sent successfully"
SAVE "campaign_logs", NOW(), lead_email, "welcome-1", "sent", ""
ELSE
TALK "Failed to send welcome email #1"
SAVE "campaign_logs", NOW(), lead_email, "welcome-1", "failed", result[0].error
END IF
' Step 2: Schedule Follow-up Emails
SET SCHEDULE "0 9 * * *" DATEADD(TODAY(), 2, "day"), "send-welcome-2.bas"
SET SCHEDULE "0 9 * * *" DATEADD(TODAY(), 5, "day"), "send-welcome-3.bas"
SET SCHEDULE "0 9 * * *" DATEADD(TODAY(), 8, "day"), "send-welcome-4.bas"
SET SCHEDULE "0 9 * * *" DATEADD(TODAY(), 14, "day"), "send-welcome-5.bas"
' Step 3: Add to CRM and Score Lead
SAVE "leads", lead_email, lead_name, lead_source, "Welcome Series", "nurturing", NOW()
' Calculate initial lead score
WITH score_data
.email = lead_email
.name = lead_name
.source = lead_source
.form_submissions = 1
END WITH
lead_score = SCORE LEAD score_data
TALK "Initial lead score: " + lead_score.score + " (Grade: " + lead_score.grade + ")"
' Step 4: Track Campaign Enrollment
SAVE "campaign_enrollments", "Welcome Series", lead_email, lead_name, NOW(), "active", 1, 5
' Step 5: Send to WhatsApp if phone provided
lead_phone = GET BOT MEMORY "lead_phone_" + lead_email
IF NOT ISEMPTY(lead_phone) THEN
WITH wa_vars
.name = lead_name
END WITH
wa_result = SEND TEMPLATE "welcome-whatsapp", "whatsapp", lead_phone, wa_vars
IF wa_result[0].success THEN
TALK "WhatsApp welcome sent"
END IF
END IF
' Output Summary
TALK ""
TALK "Welcome Campaign Started"
TALK "Lead: " + lead_name + " <" + lead_email + ">"
TALK "Score: " + lead_score.score + " (" + lead_score.grade + ")"
TALK "Status: " + lead_score.status
TALK "Emails Scheduled: 5"
TALK "Campaign Duration: 14 days"
WITH campaign_status
.campaign = "Welcome Series"
.lead = lead_email
.initial_score = lead_score.score
.grade = lead_score.grade
.emails_scheduled = 5
.next_email_date = DATEADD(TODAY(), 2, "day")
.status = "active"
END WITH
RETURN campaign_status