diff --git a/Cargo.toml b/Cargo.toml index ffeff2c..b2f3bb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,14 @@ path = "src/lib.rs" name = "runner" path = "src/bin/runner.rs" +[[example]] +name = "engine" +path = "examples/engine/main.rs" + +[[example]] +name = "freezone" +path = "examples/freezone/main.rs" + [dependencies] anyhow = "1.0" redis = { version = "0.24", features = ["aio", "tokio-comp"] } @@ -24,8 +32,10 @@ uuid = { version = "1.6", features = ["v4", "serde"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } osiris_derive = { path = "osiris_derive" } +lettre = "0.11" rhai = { version = "1.21.0", features = ["std", "sync", "serde"] } env_logger = "0.10" +reqwest = { version = "0.11", features = ["json"] } [dev-dependencies] tempfile = "3.8" diff --git a/docs/CREATING_NEW_OBJECTS.md b/docs/CREATING_NEW_OBJECTS.md new file mode 100644 index 0000000..d1a1110 --- /dev/null +++ b/docs/CREATING_NEW_OBJECTS.md @@ -0,0 +1,232 @@ +# Guide: Creating New OSIRIS Objects + +This guide explains how to properly create new object types in OSIRIS that integrate with the Rhai scripting engine and context storage. + +## Step-by-Step Process + +### 1. Create the Object Module + +Create a new file in the appropriate directory under `src/objects/`: +- `src/objects/legal/` for legal objects +- `src/objects/money/` for financial objects +- `src/objects/heroledger/` for HeroLedger objects +- etc. + +### 2. Define the Object Struct + +**CRITICAL**: The struct MUST derive `crate::DeriveObject` to automatically implement the `Object` trait. + +```rust +use crate::store::BaseData; +use serde::{Deserialize, Serialize}; + +/// Your object description +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)] +pub struct YourObject { + /// Base data for object storage (REQUIRED) + pub base_data: BaseData, + + /// Your custom fields + pub title: String, + pub status: YourStatus, + // ... other fields +} +``` + +**Required traits:** +- `Debug` - for debugging +- `Clone` - required by Object trait +- `Serialize, Deserialize` - for JSON serialization +- `PartialEq` - for comparisons +- `crate::DeriveObject` - **CRITICAL** - auto-implements Object trait + +**Required field:** +- `base_data: BaseData` - MUST be present for object storage + +### 3. Implement Constructor and Methods + +```rust +impl YourObject { + /// Create a new object + pub fn new(id: u32) -> Self { + let base_data = BaseData::with_id(id, String::new()); + Self { + base_data, + title: String::new(), + status: YourStatus::default(), + // ... initialize other fields + } + } + + /// Fluent builder methods + pub fn title(mut self, title: impl ToString) -> Self { + self.title = title.to_string(); + self + } + + // ... other methods +} +``` + +### 4. Create Rhai Bindings Module + +Create `rhai.rs` in the same directory: + +```rust +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; + +use super::{YourObject, YourStatus}; + +/// Register your modules with the Rhai engine +pub fn register_your_modules(parent_module: &mut Module) { + // Register custom types + parent_module.set_custom_type::("YourObject"); + parent_module.set_custom_type::("YourStatus"); + + // Merge functions + let your_module = exported_module!(rhai_your_module); + parent_module.merge(&your_module); +} + +#[export_module] +mod rhai_your_module { + use super::YourObject; + use ::rhai::EvalAltResult; + + // Constructor + #[rhai_fn(name = "new_your_object", return_raw)] + pub fn new_your_object(id: i64) -> Result> { + Ok(YourObject::new(id as u32)) + } + + // Builder methods + #[rhai_fn(name = "title", return_raw)] + pub fn set_title( + obj: YourObject, + title: String, + ) -> Result> { + Ok(obj.title(title)) + } + + // Getters + #[rhai_fn(name = "title", pure)] + pub fn get_title(obj: &mut YourObject) -> String { + obj.title.clone() + } +} + +// CustomType implementations +impl CustomType for YourObject { + fn build(mut builder: TypeBuilder) { + builder.with_name("YourObject"); + } +} + +impl CustomType for YourStatus { + fn build(mut builder: TypeBuilder) { + builder.with_name("YourStatus"); + } +} +``` + +### 5. Update Module Exports + +In `mod.rs` of your object category: + +```rust +pub mod your_object; +pub mod rhai; + +pub use your_object::{YourObject, YourStatus}; +pub use rhai::register_your_modules; +``` + +In `src/objects/mod.rs`: + +```rust +pub mod your_category; + +pub use your_category::{YourObject, YourStatus}; +``` + +### 6. Register in Engine (CRITICAL STEP) + +In `src/engine.rs`, add the save registration in the `OsirisPackage` definition: + +```rust +def_package! { + pub OsirisPackage(module) : StandardPackage { + // ... existing registrations ... + + // Add your object's save method + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, obj: crate::objects::YourObject| ctx.save_object(obj)); + + // ... existing registrations ... + + // Register your modules + register_your_modules(module); + } +} +``` + +Also add the import at the top of `engine.rs`: + +```rust +use crate::objects::your_category::rhai::register_your_modules; +``` + +### 7. Create Example Script + +Create `examples/engine/XX_your_object.rhai`: + +```rhai +print("=== Your Object Example ===\n"); + +// Get context +let ctx = get_context(["alice", "bob"]); + +// Create object +let obj = new_your_object(1) + .title("Example Title"); + +print("Object created: " + obj.title()); + +// Store in context +ctx.save(obj); +print("Object stored"); + +print("\n=== Example Complete ==="); +``` + +## Checklist + +Before considering your object complete, verify: + +- [ ] Struct derives `crate::DeriveObject` +- [ ] Struct has `base_data: BaseData` field +- [ ] Rhai module created with `register_*_modules` function +- [ ] Custom types registered with `set_custom_type` +- [ ] Module exported in `mod.rs` files +- [ ] Save method registered in `src/engine.rs` +- [ ] Module registration added to `OsirisPackage` in `src/engine.rs` +- [ ] Example script created and tested +- [ ] Example runs successfully with `cargo run --example engine examples/engine/XX_your_object.rhai` + +## Common Mistakes to Avoid + +1. **Forgetting `crate::DeriveObject`** - Without this, the Object trait won't be implemented +2. **Missing `base_data` field** - Required for all storable objects +3. **Not registering save in engine.rs** - The save method MUST be in engine.rs, not context.rs +4. **Not calling `set_custom_type`** - Rhai won't recognize your type +5. **Not merging the exported module** - Your functions won't be available + +## Example: Contract Object + +See the Contract object implementation as a reference: +- Struct: `src/objects/legal/contract.rs` +- Rhai bindings: `src/objects/legal/rhai.rs` +- Module exports: `src/objects/legal/mod.rs` and `src/objects/mod.rs` +- Engine registration: `src/engine.rs` (line ~110 and ~138) +- Example: `examples/engine/12_contract.rhai` diff --git a/docs/FREEZONE_IMPLEMENTATION_TODO.md b/docs/FREEZONE_IMPLEMENTATION_TODO.md new file mode 100644 index 0000000..4e86402 --- /dev/null +++ b/docs/FREEZONE_IMPLEMENTATION_TODO.md @@ -0,0 +1,113 @@ +# Freezone Implementation TODO + +## Summary +The freezone.rhai example has been created and demonstrates a complete registration flow. However, some Rhai bindings need to be implemented to make it fully functional. + +## Required Implementations + +### 1. Ethereum Wallet Function +**Location:** `src/objects/money/rhai.rs` + +Add a `new_ethereum_wallet()` function that creates an Ethereum wallet: + +```rust +#[rhai_fn(name = "new_ethereum_wallet", return_raw)] +pub fn new_ethereum_wallet() -> Result> { + // Generate new Ethereum wallet + // Return wallet with address, private key (encrypted), network + Ok(EthereumWallet::new()) +} +``` + +The wallet should have: +- `.owner_id(id)` - set owner +- `.network(network)` - set network (mainnet/testnet) +- `.get_address()` - get Ethereum address + +### 2. Accounting Module +**Location:** `src/objects/accounting/` + +Create Invoice and Expense objects (files were created but need to be integrated): + +**Invoice:** +- `new_invoice(id)` - constructor +- `.invoice_number(num)`, `.customer_id(id)`, `.amount(amt)`, `.currency(cur)`, `.description(desc)` +- `.send()`, `.mark_paid()`, `.mark_overdue()`, `.cancel()` +- Getters: `.invoice_number()`, `.customer_id()`, `.amount()`, `.status()` + +**Expense:** +- `new_expense(id)` - constructor +- `.user_id(id)`, `.amount(amt)`, `.currency(cur)`, `.description(desc)`, `.category(cat)`, `.invoice_id(id)` +- `.approve()`, `.mark_paid()`, `.reject()` +- Getters: `.user_id()`, `.amount()`, `.status()`, `.category()` + +### 3. Payment Request Enhancements +**Location:** `src/objects/money/payments.rs` and `rhai.rs` + +Add `new_payment_request()` function with builder API: +- `.amount(amt)` +- `.currency(cur)` +- `.description(desc)` +- `.callback_url(url)` +- `.merchant_reference(ref)` + +### 4. KYC Info Enhancements +**Location:** `src/objects/kyc/info.rs` and `rhai.rs` + +Add missing builder methods: +- `.document_type(type)` - passport, id_card, drivers_license +- `.document_number(num)` +- `.verified(bool)` - mark as verified + +### 5. Engine Registration +**Location:** `src/engine.rs` + +Add to `OsirisPackage`: +```rust +// Register Accounting modules +register_accounting_modules(module); + +// Add save methods for Invoice and Expense +FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, invoice: crate::objects::Invoice| ctx.save_object(invoice)); +FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, expense: crate::objects::Expense| ctx.save_object(expense)); +``` + +### 6. Module Exports +**Location:** `src/objects/mod.rs` + +Add: +```rust +pub mod accounting; +pub use accounting::{Invoice, Expense}; +``` + +## Current Freezone Flow + +The freezone.rhai example demonstrates: + +1. **Public Key Registration** - User provides public key +2. **Personal Information** - Collect name, email, create KYC info +3. **Terms & Conditions** - Create and sign contract +4. **Email Verification** - Generate code, send email, verify +5. **Crypto Wallet Creation** - Create TFT account + Ethereum wallet +6. **Payment Processing** - Pesapal payment session, user pays, record transaction +7. **KYC Verification** - KYC session, user completes verification, callback with results +8. **User Registration** - Create final user object + +## Testing + +Once implementations are complete, run: +```bash +cargo run --example freezone +``` + +Expected output: Complete freezone registration flow with all 8 steps executing successfully. + +## Notes + +- The example uses simulated callbacks for payment and KYC +- Ethereum wallet generation should use a proper library (e.g., ethers-rs) +- Payment integration with Pesapal is mocked but shows the expected flow +- KYC callback demonstrates how verified data would be received and stored diff --git a/examples/engine/01_note.rhai b/examples/engine/01_note.rhai new file mode 100644 index 0000000..1f96a0b --- /dev/null +++ b/examples/engine/01_note.rhai @@ -0,0 +1,53 @@ +// Example 1: Creating and working with Notes + +print("=== Note Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create notes with builder pattern +print("\n2. Creating notes..."); + +let meeting_note = note("notes") + .title("Meeting Notes") + .content("Discussed OSIRIS integration and Rhai packages") + .tag("category", "work") + .tag("priority", "high") + .mime("text/markdown"); + +print(" ✓ Created meeting note with tags and mime type"); + +let quick_note = note("notes") + .title("Quick Reminder") + .content("Remember to push changes") + .tag("category", "personal"); + +print(" ✓ Created quick reminder note"); + +let project_note = note("notes") + .title("Project Plan") + .content("1. Design\n2. Implement\n3. Test") + .tag("project", "osiris") + .tag("status", "in-progress"); + +print(" ✓ Created project plan note"); + +// Save notes to context +print("\n3. Saving notes to context..."); +ctx.save(meeting_note); +print(" ✓ Saved meeting note"); + +ctx.save(quick_note); +print(" ✓ Saved quick note"); + +ctx.save(project_note); +print(" ✓ Saved project note"); + +// List all notes in context +print("\n4. Listing notes in context..."); +let note_ids = ctx.list("notes"); +print(" ✓ Found " + note_ids.len() + " notes in context"); + +print("\n=== Note Example Complete ==="); diff --git a/examples/engine/02_event.rhai b/examples/engine/02_event.rhai new file mode 100644 index 0000000..d6c6fa1 --- /dev/null +++ b/examples/engine/02_event.rhai @@ -0,0 +1,44 @@ +// Example 2: Creating and working with Events + +print("=== Event Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "charlie"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create events with builder pattern +print("\n2. Creating events..."); + +let standup = event("calendar", "Daily Standup") + .description("Quick sync on progress and blockers"); + +print(" ✓ Created daily standup event"); + +let review = event("calendar", "Sprint Review") + .description("Demo completed features to stakeholders"); + +print(" ✓ Created sprint review event"); + +let workshop = event("calendar", "OSIRIS Workshop") + .description("Deep dive into OSIRIS architecture and Rhai integration"); + +print(" ✓ Created workshop event"); + +// Save events to context +print("\n3. Saving events to context..."); +ctx.save(standup); +print(" ✓ Saved standup event"); + +ctx.save(review); +print(" ✓ Saved review event"); + +ctx.save(workshop); +print(" ✓ Saved workshop event"); + +// List all events in context +print("\n4. Listing events in context..."); +let event_ids = ctx.list("calendar"); +print(" ✓ Found " + event_ids.len() + " events in context"); + +print("\n=== Event Example Complete ==="); diff --git a/examples/engine/03_user.rhai b/examples/engine/03_user.rhai new file mode 100644 index 0000000..6876b9f --- /dev/null +++ b/examples/engine/03_user.rhai @@ -0,0 +1,54 @@ +// Example 3: Creating and working with Users + +print("=== User Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create users with builder pattern +print("\n2. Creating users..."); + +let alice = new_user() + .username("alice") + .add_email("alice@example.com"); + +print(" ✓ Created user: alice"); + +let bob = new_user() + .username("bob") + .add_email("bob@example.com"); + +print(" ✓ Created user: bob"); + +let charlie = new_user() + .username("charlie") + .add_email("charlie@example.com"); + +print(" ✓ Created user: charlie"); + +// Display user info +print("\n3. User information..."); +print(" Alice ID: " + alice.get_id()); +print(" Alice username: " + alice.get_username()); +print(" Alice email: " + alice.get_email()); + +print(" Bob ID: " + bob.get_id()); +print(" Bob username: " + bob.get_username()); + +print(" Charlie ID: " + charlie.get_id()); +print(" Charlie username: " + charlie.get_username()); + +// Save users to context +print("\n4. Saving users to context..."); +let alice_id = ctx.save(alice); +print(" ✓ Saved alice with ID: " + alice_id); + +let bob_id = ctx.save(bob); +print(" ✓ Saved bob with ID: " + bob_id); + +let charlie_id = ctx.save(charlie); +print(" ✓ Saved charlie with ID: " + charlie_id); + +print("\n=== User Example Complete ==="); diff --git a/examples/engine/04_group.rhai b/examples/engine/04_group.rhai new file mode 100644 index 0000000..bfbf8d2 --- /dev/null +++ b/examples/engine/04_group.rhai @@ -0,0 +1,54 @@ +// Example 4: Creating and working with Groups + +print("=== Group Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create groups with builder pattern +print("\n2. Creating groups..."); + +let dev_team = new_group() + .name("Development Team") + .description("Core development team members"); + +print(" ✓ Created group: Development Team"); + +let ops_team = new_group() + .name("Operations Team") + .description("Infrastructure and operations team"); + +print(" ✓ Created group: Operations Team"); + +let admin_group = new_group() + .name("Administrators") + .description("System administrators with full access"); + +print(" ✓ Created group: Administrators"); + +// Display group info +print("\n3. Group information..."); +print(" Dev Team ID: " + dev_team.get_id()); +print(" Dev Team name: " + dev_team.get_name()); +print(" Dev Team description: " + dev_team.get_description()); + +print(" Ops Team ID: " + ops_team.get_id()); +print(" Ops Team name: " + ops_team.get_name()); + +print(" Admin Group ID: " + admin_group.get_id()); +print(" Admin Group name: " + admin_group.get_name()); + +// Save groups to context +print("\n4. Saving groups to context..."); +let dev_id = ctx.save(dev_team); +print(" ✓ Saved Development Team with ID: " + dev_id); + +let ops_id = ctx.save(ops_team); +print(" ✓ Saved Operations Team with ID: " + ops_id); + +let admin_id = ctx.save(admin_group); +print(" ✓ Saved Administrators with ID: " + admin_id); + +print("\n=== Group Example Complete ==="); diff --git a/examples/engine/05_account.rhai b/examples/engine/05_account.rhai new file mode 100644 index 0000000..2ef00b6 --- /dev/null +++ b/examples/engine/05_account.rhai @@ -0,0 +1,55 @@ +// Example 5: Creating and working with Accounts + +print("=== Account Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create accounts with builder pattern +print("\n2. Creating accounts..."); + +let checking = new_account() + .address("checking_001") + .currency("USD"); + +print(" ✓ Created checking account"); + +let savings = new_account() + .address("savings_001") + .currency("USD"); + +print(" ✓ Created savings account"); + +let crypto = new_account() + .address("0x1234567890abcdef") + .currency("ETH"); + +print(" ✓ Created crypto account"); + +// Display account info +print("\n3. Account information..."); +print(" Checking ID: " + checking.get_id()); +print(" Checking address: " + checking.get_address()); +print(" Checking currency: " + checking.get_currency()); + +print(" Savings ID: " + savings.get_id()); +print(" Savings address: " + savings.get_address()); + +print(" Crypto ID: " + crypto.get_id()); +print(" Crypto address: " + crypto.get_address()); +print(" Crypto currency: " + crypto.get_currency()); + +// Save accounts to context +print("\n4. Saving accounts to context..."); +let checking_id = ctx.save(checking); +print(" ✓ Saved checking account with ID: " + checking_id); + +let savings_id = ctx.save(savings); +print(" ✓ Saved savings account with ID: " + savings_id); + +let crypto_id = ctx.save(crypto); +print(" ✓ Saved crypto account with ID: " + crypto_id); + +print("\n=== Account Example Complete ==="); diff --git a/examples/engine/06_dnszone.rhai b/examples/engine/06_dnszone.rhai new file mode 100644 index 0000000..a0876d7 --- /dev/null +++ b/examples/engine/06_dnszone.rhai @@ -0,0 +1,50 @@ +// Example 6: Creating and working with DNS Zones + +print("=== DNS Zone Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create DNS zones with builder pattern +print("\n2. Creating DNS zones..."); + +let example_zone = new_dns_zone() + .domain("example.com"); + +print(" ✓ Created DNS zone: example.com"); + +let test_zone = new_dns_zone() + .domain("test.org"); + +print(" ✓ Created DNS zone: test.org"); + +let dev_zone = new_dns_zone() + .domain("dev.local"); + +print(" ✓ Created DNS zone: dev.local"); + +// Display DNS zone info +print("\n3. DNS Zone information..."); +print(" Example Zone ID: " + example_zone.get_id()); +print(" Example Zone domain: " + example_zone.get_domain()); + +print(" Test Zone ID: " + test_zone.get_id()); +print(" Test Zone domain: " + test_zone.get_domain()); + +print(" Dev Zone ID: " + dev_zone.get_id()); +print(" Dev Zone domain: " + dev_zone.get_domain()); + +// Save DNS zones to context +print("\n4. Saving DNS zones to context..."); +let example_id = ctx.save(example_zone); +print(" ✓ Saved example.com with ID: " + example_id); + +let test_id = ctx.save(test_zone); +print(" ✓ Saved test.org with ID: " + test_id); + +let dev_id = ctx.save(dev_zone); +print(" ✓ Saved dev.local with ID: " + dev_id); + +print("\n=== DNS Zone Example Complete ==="); diff --git a/examples/engine/07_kyc.rhai b/examples/engine/07_kyc.rhai new file mode 100644 index 0000000..0d6fa36 --- /dev/null +++ b/examples/engine/07_kyc.rhai @@ -0,0 +1,104 @@ +// Example 7: KYC Info and Session Management + +print("=== KYC Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create KYC info objects with builder pattern +print("\n2. Creating KYC info objects..."); + +let john_info = new_kyc_info() + .client_id("CLIENT_001") + .first_name("John") + .last_name("Doe") + .email("john.doe@example.com") + .phone("+1234567890") + .date_of_birth("1990-01-15") + .nationality("US") + .address("123 Main Street") + .city("New York") + .country("US") + .postal_code("10001") + .provider("idenfy"); + +print(" ✓ Created KYC info for John Doe"); + +let jane_info = new_kyc_info() + .client_id("CLIENT_002") + .first_name("Jane") + .last_name("Smith") + .email("jane.smith@example.com") + .phone("+9876543210") + .date_of_birth("1985-05-20") + .nationality("GB") + .provider("idenfy"); + +print(" ✓ Created KYC info for Jane Smith"); + +// Display info +print("\n3. KYC information..."); +print(" John - ID: " + john_info.get_id()); +print(" John - Client ID: " + john_info.get_client_id()); +print(" John - Name: " + john_info.get_first_name() + " " + john_info.get_last_name()); +print(" John - Email: " + john_info.get_email()); +print(" John - Provider: " + john_info.get_provider()); + +print(" Jane - ID: " + jane_info.get_id()); +print(" Jane - Client ID: " + jane_info.get_client_id()); +print(" Jane - Name: " + jane_info.get_first_name() + " " + jane_info.get_last_name()); + +// Save KYC info to context +print("\n4. Saving KYC info to context..."); +let john_id = ctx.save(john_info); +print(" ✓ Saved John's KYC info with ID: " + john_id); + +let jane_id = ctx.save(jane_info); +print(" ✓ Saved Jane's KYC info with ID: " + jane_id); + +// Create KYC verification sessions +print("\n5. Creating KYC verification sessions..."); + +let john_session = new_kyc_session("CLIENT_001", "idenfy") + .callback_url("https://example.com/kyc/callback") + .success_url("https://example.com/kyc/success") + .error_url("https://example.com/kyc/error") + .locale("en"); + +print(" ✓ Created verification session for John"); + +let jane_session = new_kyc_session("CLIENT_002", "idenfy") + .callback_url("https://example.com/kyc/callback") + .success_url("https://example.com/kyc/success") + .error_url("https://example.com/kyc/error") + .locale("en"); + +print(" ✓ Created verification session for Jane"); + +// Display session info +print("\n6. Session information..."); +print(" John's session - ID: " + john_session.get_id()); +print(" John's session - Client ID: " + john_session.get_client_id()); +print(" John's session - Provider: " + john_session.get_provider()); + +// Save KYC sessions to context +print("\n7. Saving KYC sessions to context..."); +let john_session_id = ctx.save(john_session); +print(" ✓ Saved John's session with ID: " + john_session_id); + +let jane_session_id = ctx.save(jane_session); +print(" ✓ Saved Jane's session with ID: " + jane_session_id); + +// List all KYC clients in context +print("\n8. Listing KYC clients in context..."); +let client_ids = ctx.list("kyc_clients"); +print(" ✓ Found " + client_ids.len() + " KYC clients in context"); + +// List all KYC sessions in context +print("\n9. Listing KYC sessions in context..."); +let session_ids = ctx.list("kyc_sessions"); +print(" ✓ Found " + session_ids.len() + " KYC sessions in context"); + +print("\n=== KYC Example Complete ==="); diff --git a/examples/engine/08_flow.rhai b/examples/engine/08_flow.rhai new file mode 100644 index 0000000..6139b69 --- /dev/null +++ b/examples/engine/08_flow.rhai @@ -0,0 +1,90 @@ +// Example 8: Flow Template and Instance Management + +print("=== Flow Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create a registration flow template +print("\n2. Creating registration flow template..."); + +let registration_flow = new_flow() + .name("registration_flow") + .description("User Registration Flow with KYC"); + +registration_flow.add_step("registration", "User PK Registration"); +registration_flow.add_step("kyc", "KYC Verification"); +registration_flow.add_step("email", "Email Verification"); + +print(" ✓ Created registration flow template"); +print(" Template: " + registration_flow.get_name()); +print(" Description: " + registration_flow.get_description()); + +// Save the template +print("\n3. Saving flow template to context..."); +let template_id = ctx.save(registration_flow.build()); +print(" ✓ Saved template with ID: " + template_id); + +// Create a flow instance for a user +print("\n4. Creating flow instance for user..."); +let user_id = "user_123"; + +let flow_instance = new_flow_instance( + "registration_flow_user_123", + "registration_flow", + user_id +); + +// Initialize steps (in real scenario, would get from template) +flow_instance.start(); + +print(" ✓ Created and started flow instance"); +print(" Instance name: " + flow_instance.get_name()); +print(" Template: " + flow_instance.get_template_name()); +print(" Entity ID: " + flow_instance.get_entity_id()); +print(" Status: " + flow_instance.get_status()); + +// Save the instance +print("\n5. Saving flow instance to context..."); +let instance_id = ctx.save(flow_instance); +print(" ✓ Saved instance with ID: " + instance_id); + +// Simulate completing steps +print("\n6. Simulating step completion..."); + +// Complete registration step +flow_instance.complete_step("registration"); +print(" ✓ Completed 'registration' step"); + +// Save updated instance +ctx.save(flow_instance); + +// Complete KYC step +flow_instance.complete_step("kyc"); +print(" ✓ Completed 'kyc' step"); + +// Save updated instance +ctx.save(flow_instance); + +// Complete email step +flow_instance.complete_step("email"); +print(" ✓ Completed 'email' step"); + +// Save final instance +ctx.save(flow_instance); + +print(" Final status: " + flow_instance.get_status()); + +// List all flow templates +print("\n7. Listing flow templates in context..."); +let template_ids = ctx.list("flow_templates"); +print(" ✓ Found " + template_ids.len() + " flow templates"); + +// List all flow instances +print("\n8. Listing flow instances in context..."); +let instance_ids = ctx.list("flow_instances"); +print(" ✓ Found " + instance_ids.len() + " flow instances"); + +print("\n=== Flow Example Complete ==="); diff --git a/examples/engine/09_money.rhai b/examples/engine/09_money.rhai new file mode 100644 index 0000000..146186a --- /dev/null +++ b/examples/engine/09_money.rhai @@ -0,0 +1,119 @@ +// Example 9: Money - Accounts, Assets, and Transactions + +print("=== Money Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob", "charlie"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create an asset (e.g., a token) +print("\n2. Creating a digital asset..."); + +let tft_asset = new_asset() + .address("TFT_ASSET_001") + .asset_type("utility_token") + .issuer(1) + .supply(1000000.0); + +print(" ✓ Created TFT asset"); +print(" Address: " + tft_asset.get_address()); +print(" Type: " + tft_asset.get_asset_type()); +print(" Supply: " + tft_asset.get_supply()); + +// Save the asset +print("\n3. Saving asset to context..."); +let asset_id = ctx.save(tft_asset); +print(" ✓ Saved asset with ID: " + asset_id); + +// Create accounts for users +print("\n4. Creating user accounts..."); + +let alice_account = new_account() + .owner_id(100) + .address("ALICE_TFT_ACCOUNT") + .balance(10000.0) + .currency("TFT") + .assetid(1); + +print(" ✓ Created Alice's account"); +print(" Owner ID: " + alice_account.get_owner_id()); +print(" Address: " + alice_account.get_address()); +print(" Balance: " + alice_account.get_balance() + " " + alice_account.get_currency()); + +let bob_account = new_account() + .owner_id(101) + .address("BOB_TFT_ACCOUNT") + .balance(5000.0) + .currency("TFT") + .assetid(1); + +print(" ✓ Created Bob's account"); +print(" Owner ID: " + bob_account.get_owner_id()); +print(" Address: " + bob_account.get_address()); +print(" Balance: " + bob_account.get_balance() + " " + bob_account.get_currency()); + +// Save accounts +print("\n5. Saving accounts to context..."); +let alice_account_id = ctx.save(alice_account); +print(" ✓ Saved Alice's account with ID: " + alice_account_id); + +let bob_account_id = ctx.save(bob_account); +print(" ✓ Saved Bob's account with ID: " + bob_account_id); + +// Create a transaction +print("\n6. Creating a transaction..."); + +let transaction = new_transaction() + .source(100) + .destination(101) + .amount(500.0) + .assetid(1); + +print(" ✓ Created transaction"); +print(" From: " + transaction.get_source()); +print(" To: " + transaction.get_destination()); +print(" Amount: " + transaction.get_amount()); +print(" Asset ID: " + transaction.get_assetid()); + +// Save transaction +print("\n7. Saving transaction to context..."); +let tx_id = ctx.save(transaction); +print(" ✓ Saved transaction with ID: " + tx_id); + +// Create a multi-currency account +print("\n8. Creating multi-currency account..."); + +let charlie_usd_account = new_account() + .owner_id(102) + .address("CHARLIE_USD_ACCOUNT") + .balance(25000.0) + .currency("USD") + .assetid(2); + +print(" ✓ Created Charlie's USD account"); +print(" Balance: " + charlie_usd_account.get_balance() + " " + charlie_usd_account.get_currency()); + +let charlie_id = ctx.save(charlie_usd_account); +print(" ✓ Saved Charlie's account with ID: " + charlie_id); + +// List all accounts +print("\n9. Listing all accounts in context..."); +let account_ids = ctx.list("accounts"); +print(" ✓ Found " + account_ids.len() + " accounts"); + +// List all assets +print("\n10. Listing all assets in context..."); +let asset_ids = ctx.list("assets"); +print(" ✓ Found " + asset_ids.len() + " assets"); + +// List all transactions +print("\n11. Listing all transactions in context..."); +let tx_ids = ctx.list("transactions"); +print(" ✓ Found " + tx_ids.len() + " transactions"); + +print("\n=== Money Example Complete ==="); +print("Summary:"); +print(" - Created 1 asset (TFT)"); +print(" - Created 3 accounts (Alice TFT, Bob TFT, Charlie USD)"); +print(" - Created 1 transaction (Alice → Bob: 500 TFT)"); diff --git a/examples/engine/10_email.rhai b/examples/engine/10_email.rhai new file mode 100644 index 0000000..13cad05 --- /dev/null +++ b/examples/engine/10_email.rhai @@ -0,0 +1,103 @@ +// Example 10: Communication - Email Verification + +print("=== Communication & Email Verification Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create verification for a user +print("\n2. Creating verification..."); + +let verification = new_verification( + "user_123", + "alice@example.com" +) +.callback_url("https://example.com/verify"); + +print(" ✓ Created verification"); +print(" Entity ID: " + verification.get_entity_id()); +print(" Contact: " + verification.get_contact()); +print(" Code: " + verification.get_code()); +print(" Nonce: " + verification.get_nonce()); +print(" Verification URL: " + verification.get_verification_url()); +print(" Status: " + verification.get_status()); + +// Create email client +print("\n3. Creating email client..."); +let email_client = new_email_client(); +print(" ✓ Email client created"); + +// Send verification email with link +print("\n4. Sending verification email..."); +email_client.send_verification_link(verification); +verification.mark_sent(); +print(" ✓ Verification email sent"); +print(" Status: " + verification.get_status()); + +// Save the verification +print("\n5. Saving verification to context..."); +let verification_id = ctx.save(verification); +print(" ✓ Saved verification with ID: " + verification_id); + +// Simulate user clicking link (nonce verification) +print("\n6. Simulating URL click verification..."); +let nonce = verification.get_nonce(); +print(" User clicks: " + verification.get_verification_url()); +verification.verify_nonce(nonce); +print(" ✓ Verification successful via URL!"); +print(" Status: " + verification.get_status()); + +// Save verified status +ctx.save(verification); + +// Create another verification for code-based flow +print("\n7. Creating code-based verification for Bob..."); + +let bob_verification = new_verification( + "user_456", + "bob@example.com" +); + +print(" ✓ Created verification for Bob"); +print(" Contact: " + bob_verification.get_contact()); +print(" Code: " + bob_verification.get_code()); + +// Send code via email +print("\n8. Sending code-based verification email..."); +email_client.send_verification_code(bob_verification); +bob_verification.mark_sent(); +print(" ✓ Code sent"); + +// Save Bob's verification +let bob_verification_id = ctx.save(bob_verification); +print(" ✓ Saved with ID: " + bob_verification_id); + +// Simulate user entering wrong code +print("\n9. Simulating code verification attempts..."); +print(" Attempt 1 with wrong code..."); +try { + bob_verification.verify_code("000000"); + print(" ✗ Unexpected success"); +} catch(err) { + print(" ✗ Verification failed (expected): " + err); + print(" Attempts: " + bob_verification.get_attempts()); +} + +// Verify with correct code +print("\n10. Verifying with correct code..."); +let correct_code = bob_verification.get_code(); +print(" Using code: " + correct_code); +bob_verification.verify_code(correct_code); +print(" ✓ Verification successful!"); +print(" Status: " + bob_verification.get_status()); + +ctx.save(bob_verification); + +// List all verifications +print("\n11. Listing all verifications in context..."); +let verification_ids = ctx.list("verifications"); +print(" ✓ Found " + verification_ids.len() + " verifications"); + +print("\n=== Communication & Email Verification Example Complete ==="); diff --git a/examples/engine/11_payments.rhai b/examples/engine/11_payments.rhai new file mode 100644 index 0000000..bde219f --- /dev/null +++ b/examples/engine/11_payments.rhai @@ -0,0 +1,104 @@ +// Example 11: Payment Provider - Pesapal Integration + +print("=== Payment Provider & Pesapal Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create Pesapal payment client (sandbox mode for testing) +print("\n2. Creating Pesapal payment client (sandbox)..."); +let payment_client = new_payment_client_pesapal_sandbox( + 1, // Client ID + "qkio1BGGYAXTu2JOfm7XSXNruoZsrqEW", // Consumer key + "osGQ364R49cXKeOYSpaOnT++rHs=" // Consumer secret +); +print(" ✓ Payment client created"); + +// Create a payment request +print("\n3. Creating payment request..."); +let payment_request = new_payment_request( + "ORDER_" + timestamp(), // Merchant reference + 2500.00, // Amount + "KES", // Currency (Kenyan Shillings) + "Payment for Premium Subscription", // Description + "https://example.com/payment/callback" // Callback URL +); + +// Add customer details +payment_request + .customer_email("customer@example.com") + .customer_phone("+254712345678") + .customer_name("John", "Doe") + .redirect_url("https://example.com/payment/success"); + +print(" ✓ Payment request created"); +print(" Merchant Reference: ORDER_" + timestamp()); +print(" Amount: 2500.00 KES"); +print(" Description: Payment for Premium Subscription"); + +// Create payment link +print("\n4. Creating payment link..."); +let payment_response = payment_client.create_payment_link(payment_request); +print(" ✓ Payment link created successfully!"); +print(" Payment URL: " + payment_response.get_payment_url()); +print(" Order Tracking ID: " + payment_response.get_order_tracking_id()); +print(" Merchant Reference: " + payment_response.get_merchant_reference()); +print(" Status: " + payment_response.get_status()); + +// In a real scenario, you would: +// 1. Redirect the user to payment_response.get_payment_url() +// 2. User completes payment on Pesapal +// 3. Pesapal sends callback to your callback URL +// 4. You check payment status using the order tracking ID + +// Simulate checking payment status +print("\n5. Checking payment status..."); +let order_tracking_id = payment_response.get_order_tracking_id(); +let payment_status = payment_client.get_payment_status(order_tracking_id); +print(" ✓ Payment status retrieved"); +print(" Order Tracking ID: " + order_tracking_id); +print(" Status: " + payment_status.get_status()); +print(" Amount: " + payment_status.get_amount() + " " + payment_status.get_currency()); +print(" Payment Method: " + payment_status.get_payment_method()); +print(" Transaction ID: " + payment_status.get_transaction_id()); + +// Create another payment for a different scenario +print("\n6. Creating another payment (smaller amount)..."); +let small_payment = new_payment_request( + "ORDER_SMALL_" + timestamp(), + 150.00, + "KES", + "Coffee Purchase", + "https://example.com/payment/callback" +); + +small_payment + .customer_email("coffee@example.com") + .customer_phone("+254798765432"); + +let small_payment_response = payment_client.create_payment_link(small_payment); +print(" ✓ Small payment link created"); +print(" Amount: 150.00 KES"); +print(" Payment URL: " + small_payment_response.get_payment_url()); + +// Example with production client (commented out - requires real credentials) +print("\n7. Production client example (not executed)..."); +print(" // For production, use:"); +print(" // let prod_client = new_payment_client_pesapal("); +print(" // \"your_production_consumer_key\","); +print(" // \"your_production_consumer_secret\""); +print(" // );"); + +print("\n=== Payment Provider & Pesapal Example Complete ==="); +print("\nNote: This example makes REAL API calls to Pesapal sandbox."); +print("The credentials used are for testing purposes only."); +print("\nPesapal Payment Flow:"); +print("1. Create payment request with amount, currency, and customer details"); +print("2. Get payment URL from Pesapal"); +print("3. Redirect customer to payment URL"); +print("4. Customer completes payment (M-PESA, Card, etc.)"); +print("5. Pesapal sends callback to your server"); +print("6. Check payment status using order tracking ID"); +print("7. Fulfill order if payment is successful"); diff --git a/examples/engine/12_contract.rhai b/examples/engine/12_contract.rhai new file mode 100644 index 0000000..aa6d4d0 --- /dev/null +++ b/examples/engine/12_contract.rhai @@ -0,0 +1,95 @@ +// Example 12: Legal Contract with Signatures + +print("=== Legal Contract Example ===\n"); + +// Get a context to work with +print("1. Getting context..."); +let ctx = get_context(["alice", "bob", "charlie"]); +print(" ✓ Context created: " + ctx.context_id()); + +// Create a contract +print("\n2. Creating a new contract..."); +let contract = new_contract(1) + .title("Service Agreement") + .content("This agreement is made between Party A and Party B for the provision of software development services...") + .creator_id(101); + +print(" ✓ Contract created"); +print(" Title: " + contract.title()); +print(" Status: " + contract.status()); +print(" Creator ID: " + contract.creator_id()); + +// Add signature IDs to contract (signatures would be created separately) +print("\n3. Adding signature IDs to contract..."); +print(" (In practice, signatures would be created separately and their IDs referenced here)"); + +contract = contract + .add_signature(1001) // Alice's signature ID + .add_signature(1002); // Bob's signature ID + +print(" ✓ Signature IDs added"); +print(" Total signatures: " + contract.signature_count()); + +// Check if contract is fully signed +print("\n4. Checking signature status..."); +let required_signatures = 2; +if contract.is_fully_signed(required_signatures) { + print(" ✓ Contract is fully signed!"); + print(" Activating contract..."); + contract = contract.activate(); + print(" ✓ Contract status: " + contract.status()); +} else { + print(" ⚠ Contract needs more signatures"); + print(" Current: " + contract.signature_count() + " / Required: " + required_signatures); +} + +// Store the contract +print("\n5. Storing contract in context..."); +ctx.save(contract); +print(" ✓ Contract stored"); + +// Simulate contract completion +print("\n6. Completing contract..."); +contract = contract.complete(); +print(" ✓ Contract status: " + contract.status()); + +// Example: Contract cancellation (alternative flow) +print("\n7. Example: Creating a contract that gets cancelled..."); +let cancelled_contract = new_contract(2) + .title("Cancelled Agreement") + .content("This contract was cancelled before completion") + .creator_id(101); + +print(" ✓ Contract created"); +cancelled_contract = cancelled_contract.cancel(); +print(" ✓ Contract cancelled"); +print(" Status: " + cancelled_contract.status()); + +// Example: Removing a signature +print("\n8. Example: Removing a signature..."); +let test_contract = new_contract(3) + .title("Test Contract") + .add_signature(2001) + .add_signature(2002) + .add_signature(2003); + +print(" ✓ Contract with 3 signatures created"); +print(" Signatures before removal: " + test_contract.signature_count()); + +test_contract = test_contract.remove_signature(2002); +print(" ✓ Signature 2002 removed"); +print(" Signatures after removal: " + test_contract.signature_count()); + +print("\n=== Legal Contract Example Complete ==="); +print("\nContract Workflow:"); +print("1. Create contract with title, content, and metadata"); +print("2. Parties create signatures for the contract"); +print("3. Add signatures to contract using signature IDs"); +print("4. Check if contract is fully signed"); +print("5. Activate contract when all signatures are collected"); +print("6. Complete or cancel contract based on outcome"); +print("\nContract Statuses:"); +print("- Draft: Initial state, collecting signatures"); +print("- Active: Fully signed and in effect"); +print("- Completed: Successfully fulfilled"); +print("- Cancelled: Terminated before completion"); diff --git a/examples/engine/main.rs b/examples/engine/main.rs new file mode 100644 index 0000000..e6ab698 --- /dev/null +++ b/examples/engine/main.rs @@ -0,0 +1,84 @@ +/// OSIRIS Engine Example +/// +/// Demonstrates how to create an OSIRIS engine with the package +/// and execute multiple Rhai scripts + +use osiris::register_osiris_full; +use rhai::Engine; +use std::fs; +use std::path::Path; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== OSIRIS Engine Example ===\n"); + + // Get all .rhai files in the current directory + let example_dir = Path::new(file!()).parent().unwrap(); + let mut rhai_files: Vec<_> = fs::read_dir(example_dir)? + .filter_map(|entry| entry.ok()) + .filter(|entry| { + entry.path().extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext == "rhai") + .unwrap_or(false) + }) + .collect(); + + // Sort files alphabetically for consistent execution order + rhai_files.sort_by_key(|entry| entry.file_name()); + + if rhai_files.is_empty() { + println!("No .rhai files found in examples/engine/"); + return Ok(()); + } + + println!("Found {} Rhai script(s):\n", rhai_files.len()); + + // Execute each Rhai script with a fresh engine + for entry in rhai_files { + let path = entry.path(); + let filename = path.file_name().unwrap().to_string_lossy(); + + println!("─────────────────────────────────────"); + println!("Running: {}", filename); + println!("─────────────────────────────────────"); + + // Create a new raw engine and register all OSIRIS components + let mut engine = Engine::new_raw(); + register_osiris_full(&mut engine); + + // Set up context tags with SIGNATORIES for get_context + let mut tag_map = rhai::Map::new(); + let signatories: rhai::Array = vec![ + rhai::Dynamic::from("alice".to_string()), + rhai::Dynamic::from("bob".to_string()), + rhai::Dynamic::from("charlie".to_string()), + ]; + tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories)); + tag_map.insert("DB_PATH".into(), "/tmp/osiris_example".to_string().into()); + tag_map.insert("CONTEXT_ID".into(), "example_context".to_string().into()); + engine.set_default_tag(rhai::Dynamic::from(tag_map)); + + // Read and execute the script + let script_content = fs::read_to_string(&path)?; + + match engine.eval::(&script_content) { + Ok(result) => { + println!("\n✓ Success!"); + if !result.is_unit() { + println!("Result: {}", result); + } + } + Err(e) => { + println!("\n✗ Error: {}", e); + } + } + + println!(); + } + + println!("─────────────────────────────────────"); + println!("All scripts executed!"); + + Ok(()) +} diff --git a/examples/freezone/freezone.rhai b/examples/freezone/freezone.rhai new file mode 100644 index 0000000..799b9f4 --- /dev/null +++ b/examples/freezone/freezone.rhai @@ -0,0 +1,365 @@ +// ============================================================================ +// FREEZONE REGISTRATION FLOW +// ============================================================================ +// Complete registration flow for new freezone members including: +// 0. Freezone initialization and configuration +// 1. Public key registration +// 2. User profile creation & email verification +// 3. Terms & Conditions signing +// 4. Crypto wallet creation (TFT + ETH) +// 5. Payment processing +// 6. KYC verification with info collection + +print("=== FREEZONE REGISTRATION FLOW ===\n"); + +// ============================================================================ +// KEYPAIRS AND IDENTITIES +// ============================================================================ + +// Freezone Organization (Keypair 1) +let freezone_pubkey = "04d0aea7f0a48bcab4389753ddc2e61623dd89d800652b11d0a383eb3ea74561d730bdd06e0ca8f4cd4013907d95782a0a584313e1d91ae5ad09b663de36bfac44"; + +// User: Timur (Keypair 2) +let timur_pubkey = "04090636d0a15854c4c0b73f65b6de5f6a27a7b22d6fbf5f6d97c45476a0384fe50781444c33f5af577e017599e4b432373fbcdcd844d8783c5e52240a14b63dc3"; + +print("Identities:"); +print(" Freezone: " + freezone_pubkey); +print(" Timur: " + timur_pubkey); +print(); + +// ============================================================================ +// INITIALIZATION: Freezone Configuration +// ============================================================================ + +print("Initialization: Freezone Configuration"); +print("─────────────────────────────────────────────────────────────"); + +// Create context with freezone as the signatory +let freezone_ctx = get_context([freezone_pubkey]); +print("✓ Freezone context created"); +print(" Context ID: " + freezone_ctx.context_id()); +print(" Signatory: Freezone (" + freezone_pubkey + ")"); + +// Configure Email Client +print("\nConfiguring Email Client..."); +let freezone_email_client = new_email_client() + .smtp_host("smtp.freezone.example") + .smtp_port(587) + .from_email("noreply@freezone.example") + .from_name("Freezone Platform"); + +print("✓ Email client configured"); +print(" SMTP Host: smtp.freezone.example"); +print(" From: noreply@freezone.example"); + +// Configure Payment Provider (Pesapal) +print("\nConfiguring Payment Provider..."); +let freezone_payment_client = new_payment_client_pesapal_sandbox( + 1, + "qkio1BGGYAXTu2JOfm7XSXNruoZsrqEW", + "osGQ364R49cXKeOYSpaOnT++rHs=" +); + +print("✓ Payment provider configured (Pesapal Sandbox)"); +print(" Provider: Pesapal"); +print(" Environment: Sandbox"); + +// Configure KYC Provider +print("\nConfiguring KYC Provider..."); +print("✓ KYC provider configured"); +print(" Provider: Freezone KYC"); +print(" Callback URL: https://freezone.example/kyc/callback"); + +// Create Freezone's own Ethereum wallet +print("\nCreating Freezone Ethereum Wallet..."); +let freezone_eth_wallet = new_ethereum_wallet() + .owner_id(999) // Freezone organization ID + .network("mainnet"); + +print("✓ Freezone Ethereum wallet created"); +print(" Address: " + freezone_eth_wallet.get_address()); +print(" Network: " + freezone_eth_wallet.get_network()); +print(" Owner: Freezone Organization (ID: 999)"); + +// Save Freezone's Ethereum account +let freezone_eth_account = new_account() + .owner_id(999) + .address(freezone_eth_wallet.get_address()) + .currency("ETH") + .balance(0.0); + +freezone_ctx.save(freezone_eth_account); +print("✓ Freezone Ethereum account saved"); + +print("\n✓ Freezone initialization complete\n"); + +// ============================================================================ +// STEP 0: Registration Flow Setup +// ============================================================================ + +print("Step 0: Registration Flow Setup"); +print("─────────────────────────────────────────────────────────────"); + +// Define registration flow steps +print("\nRegistration Flow Steps:"); +print(" 1. Public key registration"); +print(" 2. User profile creation & email verification"); +print(" 3. Terms & Conditions signing"); +print(" 4. Crypto wallet creation (TFT + ETH)"); +print(" 5. Payment processing"); +print(" 6. KYC verification with info collection"); +print(); + +// ============================================================================ +// STEP 1: Public Key Registration +// ============================================================================ + +print("Step 1: Public Key Registration"); +print("─────────────────────────────────────────────────────────────"); + +// User (Timur) provides their public key +print("User public key received: " + timur_pubkey); +print("✓ Public key validated and stored\n"); + +// ============================================================================ +// STEP 2: User Profile Creation & Email Verification +// ============================================================================ + +print("Step 2: User Profile Creation & Email Verification"); +print("─────────────────────────────────────────────────────────────"); + +// Collect basic user information +let user_name = "Timur Gordon"; +let user_email = "timur@freezone.example"; +print("Collecting user information:"); +print(" Name: " + user_name); +print(" Email: " + user_email); + +// Create user profile +let user_profile = new_user() + .username(user_name) + .pubkey(timur_pubkey) + .add_email(user_email); + +print("✓ User profile created"); +freezone_ctx.save(user_profile); +print("✓ Profile saved"); + +// Email Verification +print("\nInitiating email verification..."); +let verification = new_verification("user_1", user_email); + +print("✓ Email verification created"); +print(" Verification code: " + verification.get_code()); +print(" Nonce: " + verification.get_nonce()); + +// Prepare verification email +let verification_link = "https://freezone.example/verify?nonce=" + verification.get_nonce(); +print(" Verification link: " + verification_link); +print(" (Email sent to: " + user_email + ")"); + +// Simulate user clicking verification link and verifying +print("\n✓ User clicks verification link and verifies email"); +verification.verify_nonce(verification.get_nonce()); +print("✓ Email verified: " + verification.get_status()); + +freezone_ctx.save(verification); +print("✓ Verification saved\n"); + +// ============================================================================ +// STEP 3: Terms & Conditions Signing +// ============================================================================ + +print("Step 3: Terms & Conditions Signing"); +print("─────────────────────────────────────────────────────────────"); + +// Create Terms & Conditions contract +let terms_contract = new_contract(1) + .title("Freezone Membership Terms & Conditions") + .content("By signing this agreement, you agree to abide by all freezone rules and regulations...") + .creator_id(999); // Freezone admin + +print("✓ Terms & Conditions contract created"); +print(" Title: " + terms_contract.title()); + +// User signs the contract (add their signature ID) +let user_signature_id = 1001; +terms_contract = terms_contract.add_signature(user_signature_id); +print("✓ User signature added (ID: " + user_signature_id + ")"); + +// Activate contract once signed +if terms_contract.is_fully_signed(1) { + terms_contract = terms_contract.activate(); + print("✓ Contract activated: " + terms_contract.status()); +} + +freezone_ctx.save(terms_contract); +print("✓ Signed contract saved\n"); + +// ============================================================================ +// STEP 4: Crypto Wallet Creation +// ============================================================================ + +print("Step 4: Crypto Wallet Creation"); +print("─────────────────────────────────────────────────────────────"); + +// Create TFT crypto account for user +let tft_account = new_account() + .owner_id(1) + .currency("TFT") // ThreeFold Token + .balance(0.0); + +print("✓ TFT account created"); +print(" Owner ID: 1"); +print(" Currency: TFT"); +print(" Initial balance: 0"); + +freezone_ctx.save(tft_account); + +// Create Ethereum wallet for user +print("\nCreating Ethereum wallet..."); +let eth_wallet = new_ethereum_wallet() + .owner_id(1) + .network("mainnet"); + +print("✓ Ethereum wallet created"); +print(" Address: " + eth_wallet.get_address()); +print(" Network: mainnet"); +print(" Balance: 0 ETH"); + +// Save as account +let eth_account = new_account() + .owner_id(1) + .address(eth_wallet.get_address()) + .currency("ETH") + .balance(0.0); + +freezone_ctx.save(eth_account); +print("✓ Ethereum account saved\n"); + +// ============================================================================ +// STEP 5: Payment Processing +// ============================================================================ + +print("Step 5: Payment Processing"); +print("─────────────────────────────────────────────────────────────"); + +print("Using configured Pesapal payment client..."); + +// Create payment request for registration fee +print("\nCreating payment session..."); +let payment_request = new_payment_request() + .amount(100.0) + .currency("USD") + .description("Freezone Registration Fee") + .callback_url("https://freezone.example/payment/callback") + .merchant_reference("REG_USER_1_" + timestamp()); + +print("✓ Payment request created"); +print(" Amount: $100 USD"); +print(" Description: Freezone Registration Fee"); + +// Initiate payment with Pesapal (this would return a payment URL) +print("\nInitiating payment session with Pesapal..."); +print(" Payment URL: https://pay.pesapal.com/iframe/PesapalIframe3/Index/?OrderTrackingId=abc123"); +print(" (User would be redirected to Pesapal payment page)"); + +// Simulate user completing payment +print("\n✓ User clicks payment link and completes payment"); +print(" Payment Status: COMPLETED"); +print(" Transaction ID: TXN_" + timestamp()); + +// Create payment transaction record +let payment_tx = new_transaction() + .source(0) // External payment + .destination(1) // Freezone account + .amount(100.0) + .assetid(1); + +print("✓ Payment transaction recorded"); + +freezone_ctx.save(payment_tx); +print("✓ Transaction saved\n"); + +// ============================================================================ +// STEP 6: KYC Verification +// ============================================================================ + +print("Step 6: KYC Verification"); +print("─────────────────────────────────────────────────────────────"); + +// Create KYC session +let kyc_session = new_kyc_session("user_1", "freezone_kyc") + .callback_url("https://freezone.example/kyc/callback") + .success_url("https://freezone.example/kyc/success") + .error_url("https://freezone.example/kyc/error"); + +print("✓ KYC session created"); +print(" Client ID: " + kyc_session.get_client_id()); +print(" Provider: " + kyc_session.get_provider()); + +// Generate KYC verification URL +print("\nKYC Verification URL generated:"); +print(" https://kyc.provider.com/verify?session=kyc_session_" + timestamp()); +print(" (User would be redirected to KYC provider)"); + +// Simulate user clicking KYC link and completing verification +print("\n✓ User clicks KYC link and completes verification"); +print(" - Uploads identity document (Passport)"); +print(" - Takes selfie for liveness check"); +print(" - Provides address proof"); + +// Simulate KYC callback with verification results +print("\n✓ KYC Provider callback received:"); +print(" - Identity verification: PASSED"); +print(" - Liveness check: PASSED"); +print(" - Address verification: PASSED"); +print(" - Sanctions screening: CLEAR"); +print(" - PEP check: NOT FOUND"); +print(" - Overall Status: VERIFIED"); + +// Create KYC info with verified data from callback +print("\nStoring verified KYC information..."); +let kyc_info_verified = new_kyc_info() + .first_name("Timur") + .last_name("Gordon") + .email(user_email) + .phone("+1-555-0123") + .country("US") + .date_of_birth("1990-05-15") + .document_type("passport") + .document_number("P123456789") + .verified(true); + +freezone_ctx.save(kyc_info_verified); +freezone_ctx.save(kyc_session); +print("✓ KYC verification data saved"); +print("✓ KYC verification completed\n"); + +// ============================================================================ +// SUMMARY +// ============================================================================ + +print("═══════════════════════════════════════════════════════════════"); +print("REGISTRATION COMPLETE"); +print("═══════════════════════════════════════════════════════════════"); +print("\nUser Summary:"); +print(" Name: " + user_name); +print(" Email: " + user_email); +print(" Public Key: " + timur_pubkey); +print(" TFT Account: Created"); +print(" ETH Account: Created"); +print(" KYC Status: Verified"); +print(" Payment Status: Completed ($100 USD)"); +print(" Contract: Signed and Active"); +print("\nRegistration Flow:"); +print(" ✓ Freezone initialization (Email, Payment, KYC providers configured)"); +print(" ✓ Freezone Ethereum wallet created"); +print(" ✓ Public key registration"); +print(" ✓ User profile creation & email verification"); +print(" ✓ Terms & Conditions signed"); +print(" ✓ Crypto wallets created (TFT + ETH)"); +print(" ✓ Payment processed ($100 USD)"); +print(" ✓ KYC verification completed with verified info"); +print("\n" + user_name + " is now a verified Freezone member!"); +print("═══════════════════════════════════════════════════════════════\n"); diff --git a/examples/freezone/main.rs b/examples/freezone/main.rs new file mode 100644 index 0000000..aef6811 --- /dev/null +++ b/examples/freezone/main.rs @@ -0,0 +1,48 @@ +/// OSIRIS Freezone Registration Flow Example +/// +/// Demonstrates a complete freezone registration flow with all steps + +use osiris::register_osiris_full; +use rhai::Engine; +use std::fs; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("=== OSIRIS Freezone Registration Flow ===\n"); + + // Create a new raw engine and register all OSIRIS components + let mut engine = Engine::new_raw(); + register_osiris_full(&mut engine); + + // Set up context tags with SIGNATORIES for get_context + let mut tag_map = rhai::Map::new(); + let signatories: rhai::Array = vec![ + rhai::Dynamic::from("freezone_admin".to_string()), + rhai::Dynamic::from("kyc_officer".to_string()), + rhai::Dynamic::from("payment_processor".to_string()), + ]; + tag_map.insert("SIGNATORIES".into(), rhai::Dynamic::from(signatories)); + tag_map.insert("DB_PATH".into(), "/tmp/osiris_freezone".to_string().into()); + tag_map.insert("CONTEXT_ID".into(), "freezone_context".to_string().into()); + engine.set_default_tag(rhai::Dynamic::from(tag_map)); + + // Read and execute the freezone script + let script_path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/freezone/freezone.rhai"); + let script_content = fs::read_to_string(script_path)?; + + match engine.eval::(&script_content) { + Ok(result) => { + println!("\n✓ Freezone registration flow completed successfully!"); + if !result.is_unit() { + println!("Result: {}", result); + } + } + Err(e) => { + eprintln!("\n✗ Error: {}", e); + eprintln!("Error details: {:?}", e); + return Err(e.into()); + } + } + + Ok(()) +} diff --git a/src/context.rs b/src/context.rs index f0f48d4..d9e92e9 100644 --- a/src/context.rs +++ b/src/context.rs @@ -9,6 +9,12 @@ /// - Generic CRUD operations for any data use crate::objects::Note; +use crate::objects::heroledger::{ + user::User, + group::Group, + money::Account, + dnsrecord::DNSZone, +}; use crate::store::{GenericStore, HeroDbClient}; use rhai::{CustomType, EvalAltResult, TypeBuilder}; use std::sync::Arc; @@ -117,7 +123,8 @@ impl OsirisContext { tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async move { let mut note = Note::new(collection_clone); - note.base_data.id = id_clone.clone(); + // Parse string ID to u32, default to 0 if parsing fails + note.base_data.id = id_clone.parse::().unwrap_or(0); note.content = Some(json_content); store.put(¬e).await @@ -227,30 +234,19 @@ impl OsirisContext { } impl OsirisContext { - /// Save a Note object (typed) - pub fn save_note(&self, note: Note) -> Result> { + /// Generic save method for any Storable object + pub fn save_object(&self, object: T) -> Result> + where + T: crate::store::Storable + Send + 'static, + { let store = self.store.clone(); - let id = note.base_data.id.clone(); + let id = object.base_data().id; tokio::task::block_in_place(|| { tokio::runtime::Handle::current().block_on(async move { - store.put(¬e).await - .map_err(|e| format!("Failed to save note: {}", e))?; - Ok(id) - }) - }).map_err(|e: String| e.into()) - } - - /// Save an Event object (typed) - pub fn save_event(&self, event: crate::objects::Event) -> Result> { - let store = self.store.clone(); - let id = event.base_data.id.clone(); - - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async move { - store.put(&event).await - .map_err(|e| format!("Failed to save event: {}", e))?; - Ok(id) + store.put(&object).await + .map_err(|e| format!("Failed to save object: {}", e))?; + Ok(id.to_string()) }) }).map_err(|e: String| e.into()) } @@ -264,9 +260,13 @@ impl CustomType for OsirisContext { .with_fn("context_id", |ctx: &mut OsirisContext| ctx.context_id()) // Generic CRUD (with collection name) .with_fn("save", |ctx: &mut OsirisContext, collection: String, id: String, data: rhai::Dynamic| ctx.save(collection, id, data)) - // Typed save methods (no collection name needed) - .with_fn("save", |ctx: &mut OsirisContext, note: Note| ctx.save_note(note)) - .with_fn("save", |ctx: &mut OsirisContext, event: crate::objects::Event| ctx.save_event(event)) + // Typed save methods (no collection name needed - Rhai will pick the right one based on type) + .with_fn("save", |ctx: &mut OsirisContext, note: Note| ctx.save_object(note)) + .with_fn("save", |ctx: &mut OsirisContext, event: crate::objects::Event| ctx.save_object(event)) + .with_fn("save", |ctx: &mut OsirisContext, user: User| ctx.save_object(user)) + .with_fn("save", |ctx: &mut OsirisContext, group: Group| ctx.save_object(group)) + .with_fn("save", |ctx: &mut OsirisContext, account: Account| ctx.save_object(account)) + .with_fn("save", |ctx: &mut OsirisContext, zone: DNSZone| ctx.save_object(zone)) .with_fn("get", |ctx: &mut OsirisContext, collection: String, id: String| ctx.get(collection, id)) .with_fn("delete", |ctx: &mut OsirisContext, collection: String, id: String| ctx.delete(collection, id)) .with_fn("list", |ctx: &mut OsirisContext, collection: String| ctx.list(collection)) diff --git a/src/engine.rs b/src/engine.rs index 1fa0230..e734623 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -5,6 +5,12 @@ use crate::context::OsirisContext; use crate::objects::note::rhai::register_note_functions; use crate::objects::event::rhai::register_event_functions; +use crate::objects::heroledger::rhai::register_heroledger_modules; +use crate::objects::kyc::rhai::register_kyc_modules; +use crate::objects::flow::rhai::register_flow_modules; +use crate::objects::communication::rhai::register_communication_modules; +use crate::objects::money::rhai::register_money_modules; +use crate::objects::legal::rhai::register_legal_modules; use rhai::{Engine, def_package, FuncRegistration}; use rhai::packages::{Package, StandardPackage}; @@ -46,7 +52,7 @@ pub fn register_context_api(engine: &mut rhai::Engine) { let has_signatory = participant_keys.iter().any(|p| signatories.contains(p)); if !has_signatory { return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( - format!("Access denied: none of the participants are signatories").into(), + format!("Access denied: none of the participants are signatories. Signatories: {}", signatories.join(", ")).into(), context.position() ))); } @@ -63,15 +69,76 @@ pub fn register_context_api(engine: &mut rhai::Engine) { def_package! { /// OSIRIS package with all OSIRIS types and functions pub OsirisPackage(module) : StandardPackage { - // Register OsirisContext type + // Register OsirisContext type with all its methods module.set_custom_type::("OsirisContext"); + // Register OsirisContext methods + FuncRegistration::new("participants") + .set_into_module(module, |ctx: &mut OsirisContext| ctx.participants()); + FuncRegistration::new("context_id") + .set_into_module(module, |ctx: &mut OsirisContext| ctx.context_id()); + // Typed save methods - all named "save" for function overloading using generic save_object + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, note: crate::objects::Note| ctx.save_object(note)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, event: crate::objects::Event| ctx.save_object(event)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, user: crate::objects::heroledger::user::User| ctx.save_object(user)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, group: crate::objects::heroledger::group::Group| ctx.save_object(group)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, account: crate::objects::heroledger::money::Account| ctx.save_object(account)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, zone: crate::objects::heroledger::dnsrecord::DNSZone| ctx.save_object(zone)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, kyc_info: crate::objects::KycInfo| ctx.save_object(kyc_info)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, kyc_session: crate::objects::KycSession| ctx.save_object(kyc_session)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, flow_template: crate::objects::FlowTemplate| ctx.save_object(flow_template)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, flow_instance: crate::objects::FlowInstance| ctx.save_object(flow_instance)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, verification: crate::objects::Verification| ctx.save_object(verification)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, account: crate::objects::Account| ctx.save_object(account)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, asset: crate::objects::Asset| ctx.save_object(asset)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, transaction: crate::objects::Transaction| ctx.save_object(transaction)); + FuncRegistration::new("save") + .set_into_module(module, |ctx: &mut OsirisContext, contract: crate::objects::Contract| ctx.save_object(contract)); + FuncRegistration::new("list") + .set_into_module(module, |ctx: &mut OsirisContext, collection: String| ctx.list(collection)); + FuncRegistration::new("get") + .set_into_module(module, |ctx: &mut OsirisContext, collection: String, id: String| ctx.get(collection, id)); + FuncRegistration::new("delete") + .set_into_module(module, |ctx: &mut OsirisContext, collection: String, id: String| ctx.delete(collection, id)); + // Register Note functions register_note_functions(module); // Register Event functions register_event_functions(module); + // Register HeroLedger modules (User, Group, Account, DNSZone) + register_heroledger_modules(module); + + // Register KYC modules (KycClient, KycSession) + register_kyc_modules(module); + + // Register Flow modules (FlowTemplate, FlowInstance) + register_flow_modules(module); + + // Register Communication modules (Verification, EmailClient) + register_communication_modules(module); + + // Register Money modules (Account, Asset, Transaction, PaymentClient) + register_money_modules(module); + + // Register Legal modules (Contract) + register_legal_modules(module); + // Register get_context function with signatory-based access control FuncRegistration::new("get_context") .set_into_module(module, |context: rhai::NativeCallContext, participants: rhai::Array| -> Result> { @@ -117,11 +184,17 @@ def_package! { } } +/// Register all OSIRIS components into an engine +/// This is a convenience function that registers the complete OsirisPackage +pub fn register_osiris_full(engine: &mut Engine) { + let package = OsirisPackage::new(); + package.register_into_engine(engine); +} + /// Create a single OSIRIS engine (for backward compatibility) pub fn create_osiris_engine() -> Result> { let mut engine = Engine::new_raw(); - let package = OsirisPackage::new(); - package.register_into_engine(&mut engine); + register_osiris_full(&mut engine); Ok(engine) } diff --git a/src/lib.rs b/src/lib.rs index ba519d3..6f5d7e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,7 @@ pub use objects::{Event, Note}; pub use context::{OsirisContext, OsirisInstance, OsirisContextBuilder}; pub use engine::{ create_osiris_engine, + register_osiris_full, OsirisPackage, }; diff --git a/src/objects/accounting/expense.rs b/src/objects/accounting/expense.rs new file mode 100644 index 0000000..505fa8a --- /dev/null +++ b/src/objects/accounting/expense.rs @@ -0,0 +1,151 @@ +/// Expense Object for Accounting + +use crate::store::BaseData; +use serde::{Deserialize, Serialize}; + +/// Expense category +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ExpenseCategory { + Registration, + Subscription, + Service, + Product, + Other, +} + +impl Default for ExpenseCategory { + fn default() -> Self { + ExpenseCategory::Other + } +} + +/// Expense status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ExpenseStatus { + Pending, + Approved, + Paid, + Rejected, +} + +impl Default for ExpenseStatus { + fn default() -> Self { + ExpenseStatus::Pending + } +} + +/// Expense record +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)] +pub struct Expense { + /// Base data for object storage + pub base_data: BaseData, + + /// User/entity ID who incurred the expense + pub user_id: u32, + + /// Amount + pub amount: f64, + + /// Currency + pub currency: String, + + /// Description + pub description: String, + + /// Category + pub category: ExpenseCategory, + + /// Status + pub status: ExpenseStatus, + + /// Date incurred (unix timestamp) + pub expense_date: u64, + + /// Related invoice ID (if any) + pub invoice_id: Option, +} + +impl Expense { + /// Create a new expense + pub fn new(id: u32) -> Self { + let base_data = BaseData::with_id(id, String::new()); + let now = time::OffsetDateTime::now_utc().unix_timestamp() as u64; + Self { + base_data, + user_id: 0, + amount: 0.0, + currency: String::from("USD"), + description: String::new(), + category: ExpenseCategory::default(), + status: ExpenseStatus::default(), + expense_date: now, + invoice_id: None, + } + } + + /// Set user ID (fluent) + pub fn user_id(mut self, id: u32) -> Self { + self.user_id = id; + self + } + + /// Set amount (fluent) + pub fn amount(mut self, amount: f64) -> Self { + self.amount = amount; + self + } + + /// Set currency (fluent) + pub fn currency(mut self, currency: impl ToString) -> Self { + self.currency = currency.to_string(); + self + } + + /// Set description (fluent) + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); + self + } + + /// Set category (fluent) + pub fn category(mut self, category: ExpenseCategory) -> Self { + self.category = category; + self + } + + /// Set category from string (fluent) + pub fn category_str(mut self, category: &str) -> Self { + self.category = match category.to_lowercase().as_str() { + "registration" => ExpenseCategory::Registration, + "subscription" => ExpenseCategory::Subscription, + "service" => ExpenseCategory::Service, + "product" => ExpenseCategory::Product, + _ => ExpenseCategory::Other, + }; + self + } + + /// Set invoice ID (fluent) + pub fn invoice_id(mut self, id: u32) -> Self { + self.invoice_id = Some(id); + self + } + + /// Approve expense + pub fn approve(mut self) -> Self { + self.status = ExpenseStatus::Approved; + self + } + + /// Mark as paid + pub fn mark_paid(mut self) -> Self { + self.status = ExpenseStatus::Paid; + self + } + + /// Reject expense + pub fn reject(mut self) -> Self { + self.status = ExpenseStatus::Rejected; + self + } +} diff --git a/src/objects/accounting/invoice.rs b/src/objects/accounting/invoice.rs new file mode 100644 index 0000000..0e2f770 --- /dev/null +++ b/src/objects/accounting/invoice.rs @@ -0,0 +1,130 @@ +/// Invoice Object for Accounting + +use crate::store::BaseData; +use serde::{Deserialize, Serialize}; + +/// Invoice status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum InvoiceStatus { + Draft, + Sent, + Paid, + Overdue, + Cancelled, +} + +impl Default for InvoiceStatus { + fn default() -> Self { + InvoiceStatus::Draft + } +} + +/// Invoice for billing +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)] +pub struct Invoice { + /// Base data for object storage + pub base_data: BaseData, + + /// Invoice number + pub invoice_number: String, + + /// Customer/payer ID + pub customer_id: u32, + + /// Amount + pub amount: f64, + + /// Currency + pub currency: String, + + /// Description + pub description: String, + + /// Status + pub status: InvoiceStatus, + + /// Due date (unix timestamp) + pub due_date: Option, + + /// Payment date (unix timestamp) + pub paid_date: Option, +} + +impl Invoice { + /// Create a new invoice + pub fn new(id: u32) -> Self { + let base_data = BaseData::with_id(id, String::new()); + Self { + base_data, + invoice_number: String::new(), + customer_id: 0, + amount: 0.0, + currency: String::from("USD"), + description: String::new(), + status: InvoiceStatus::default(), + due_date: None, + paid_date: None, + } + } + + /// Set invoice number (fluent) + pub fn invoice_number(mut self, number: impl ToString) -> Self { + self.invoice_number = number.to_string(); + self + } + + /// Set customer ID (fluent) + pub fn customer_id(mut self, id: u32) -> Self { + self.customer_id = id; + self + } + + /// Set amount (fluent) + pub fn amount(mut self, amount: f64) -> Self { + self.amount = amount; + self + } + + /// Set currency (fluent) + pub fn currency(mut self, currency: impl ToString) -> Self { + self.currency = currency.to_string(); + self + } + + /// Set description (fluent) + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); + self + } + + /// Set due date (fluent) + pub fn due_date(mut self, date: u64) -> Self { + self.due_date = Some(date); + self + } + + /// Mark as sent + pub fn send(mut self) -> Self { + self.status = InvoiceStatus::Sent; + self + } + + /// Mark as paid + pub fn mark_paid(mut self) -> Self { + self.status = InvoiceStatus::Paid; + self.paid_date = Some(time::OffsetDateTime::now_utc().unix_timestamp() as u64); + self + } + + /// Mark as overdue + pub fn mark_overdue(mut self) -> Self { + self.status = InvoiceStatus::Overdue; + self + } + + /// Cancel invoice + pub fn cancel(mut self) -> Self { + self.status = InvoiceStatus::Cancelled; + self + } +} diff --git a/src/objects/accounting/mod.rs b/src/objects/accounting/mod.rs new file mode 100644 index 0000000..fa9ded3 --- /dev/null +++ b/src/objects/accounting/mod.rs @@ -0,0 +1,11 @@ +/// Accounting Module +/// +/// Provides Invoice and Expense objects for financial tracking + +pub mod invoice; +pub mod expense; +pub mod rhai; + +pub use invoice::{Invoice, InvoiceStatus}; +pub use expense::{Expense, ExpenseCategory, ExpenseStatus}; +pub use rhai::register_accounting_modules; diff --git a/src/objects/accounting/rhai.rs b/src/objects/accounting/rhai.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/objects/communication/email.rs b/src/objects/communication/email.rs new file mode 100644 index 0000000..e39b2de --- /dev/null +++ b/src/objects/communication/email.rs @@ -0,0 +1,316 @@ +/// Email Client +/// +/// Real SMTP email client for sending emails including verification emails. + +use serde::{Deserialize, Serialize}; +use super::verification::Verification; +use lettre::{ + Message, SmtpTransport, Transport, + message::{header::ContentType, MultiPart, SinglePart}, + transport::smtp::authentication::Credentials, +}; + +/// Email client with SMTP configuration +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EmailClient { + /// SMTP server hostname + pub smtp_host: String, + + /// SMTP port + pub smtp_port: u16, + + /// Username for SMTP auth + pub username: String, + + /// Password for SMTP auth + pub password: String, + + /// From address + pub from_address: String, + + /// From name + pub from_name: String, + + /// Use TLS + pub use_tls: bool, +} + +impl Default for EmailClient { + fn default() -> Self { + Self { + smtp_host: "localhost".to_string(), + smtp_port: 587, + username: String::new(), + password: String::new(), + from_address: "noreply@example.com".to_string(), + from_name: "No Reply".to_string(), + use_tls: true, + } + } +} + +impl EmailClient { + /// Create a new email client + pub fn new() -> Self { + Self::default() + } + + /// Builder: Set SMTP host + pub fn smtp_host(mut self, host: String) -> Self { + self.smtp_host = host; + self + } + + /// Builder: Set SMTP port + pub fn smtp_port(mut self, port: u16) -> Self { + self.smtp_port = port; + self + } + + /// Builder: Set username + pub fn username(mut self, username: String) -> Self { + self.username = username; + self + } + + /// Builder: Set password + pub fn password(mut self, password: String) -> Self { + self.password = password; + self + } + + /// Builder: Set from address + pub fn from_address(mut self, address: String) -> Self { + self.from_address = address; + self + } + + /// Builder: Set from name + pub fn from_name(mut self, name: String) -> Self { + self.from_name = name; + self + } + + /// Builder: Set use TLS + pub fn use_tls(mut self, use_tls: bool) -> Self { + self.use_tls = use_tls; + self + } + + /// Build SMTP transport + fn build_transport(&self) -> Result { + let creds = Credentials::new( + self.username.clone(), + self.password.clone(), + ); + + let transport = if self.use_tls { + SmtpTransport::starttls_relay(&self.smtp_host) + .map_err(|e| format!("Failed to create SMTP transport: {}", e))? + .credentials(creds) + .port(self.smtp_port) + .build() + } else { + SmtpTransport::builder_dangerous(&self.smtp_host) + .credentials(creds) + .port(self.smtp_port) + .build() + }; + + Ok(transport) + } + + /// Send a plain text email + pub fn send_email( + &self, + to: &str, + subject: &str, + body: &str, + ) -> Result<(), String> { + let from_mailbox = format!("{} <{}>", self.from_name, self.from_address) + .parse() + .map_err(|e| format!("Invalid from address: {}", e))?; + + let to_mailbox = to.parse() + .map_err(|e| format!("Invalid to address: {}", e))?; + + let email = Message::builder() + .from(from_mailbox) + .to(to_mailbox) + .subject(subject) + .body(body.to_string()) + .map_err(|e| format!("Failed to build email: {}", e))?; + + let transport = self.build_transport()?; + + transport.send(&email) + .map_err(|e| format!("Failed to send email: {}", e))?; + + Ok(()) + } + + /// Send an HTML email + pub fn send_html_email( + &self, + to: &str, + subject: &str, + html_body: &str, + text_body: Option<&str>, + ) -> Result<(), String> { + let from_mailbox = format!("{} <{}>", self.from_name, self.from_address) + .parse() + .map_err(|e| format!("Invalid from address: {}", e))?; + + let to_mailbox = to.parse() + .map_err(|e| format!("Invalid to address: {}", e))?; + + // Build multipart email with text and HTML alternatives + let text_part = if let Some(text) = text_body { + SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .body(text.to_string()) + } else { + SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .body(String::new()) + }; + + let html_part = SinglePart::builder() + .header(ContentType::TEXT_HTML) + .body(html_body.to_string()); + + let multipart = MultiPart::alternative() + .singlepart(text_part) + .singlepart(html_part); + + let email = Message::builder() + .from(from_mailbox) + .to(to_mailbox) + .subject(subject) + .multipart(multipart) + .map_err(|e| format!("Failed to build email: {}", e))?; + + let transport = self.build_transport()?; + + transport.send(&email) + .map_err(|e| format!("Failed to send email: {}", e))?; + + Ok(()) + } + + /// Send a verification email with code + pub fn send_verification_code_email( + &self, + verification: &Verification, + ) -> Result<(), String> { + let subject = "Verify your email address"; + let body = format!( + "Hello,\n\n\ + Please verify your email address by entering this code:\n\n\ + {}\n\n\ + This code will expire in 24 hours.\n\n\ + If you didn't request this, please ignore this email.", + verification.verification_code + ); + + self.send_email(&verification.contact, subject, &body) + } + + /// Send a verification email with URL link + pub fn send_verification_link_email( + &self, + verification: &Verification, + ) -> Result<(), String> { + let verification_url = verification.get_verification_url() + .ok_or_else(|| "No callback URL configured".to_string())?; + + let subject = "Verify your email address"; + + let html_body = format!( + r#" + + + + + +
+

Verify your email address

+

Hello,

+

Please verify your email address by clicking the button below:

+ Verify Email +

Or enter this verification code:

+
{}
+

This link and code will expire in 24 hours.

+

If you didn't request this, please ignore this email.

+
+ +"#, + verification_url, verification.verification_code + ); + + let text_body = format!( + "Hello,\n\n\ + Please verify your email address by visiting this link:\n\ + {}\n\n\ + Or enter this verification code: {}\n\n\ + This link and code will expire in 24 hours.\n\n\ + If you didn't request this, please ignore this email.", + verification_url, verification.verification_code + ); + + self.send_html_email( + &verification.contact, + subject, + &html_body, + Some(&text_body), + ) + } +} + +// For Rhai integration, we need a simpler synchronous wrapper +impl EmailClient { + /// Synchronous wrapper for send_verification_code_email + pub fn send_verification_code_sync(&self, verification: &Verification) -> Result<(), String> { + // In a real implementation, you'd use tokio::runtime::Runtime::new().block_on() + // For now, just simulate + println!("=== VERIFICATION CODE EMAIL ==="); + println!("To: {}", verification.contact); + println!("Code: {}", verification.verification_code); + println!("==============================="); + Ok(()) + } + + /// Synchronous wrapper for send_verification_link_email + pub fn send_verification_link_sync(&self, verification: &Verification) -> Result<(), String> { + let verification_url = verification.get_verification_url() + .ok_or_else(|| "No callback URL configured".to_string())?; + + println!("=== VERIFICATION LINK EMAIL ==="); + println!("To: {}", verification.contact); + println!("Code: {}", verification.verification_code); + println!("Link: {}", verification_url); + println!("==============================="); + Ok(()) + } +} diff --git a/src/objects/communication/mod.rs b/src/objects/communication/mod.rs new file mode 100644 index 0000000..a982720 --- /dev/null +++ b/src/objects/communication/mod.rs @@ -0,0 +1,10 @@ +/// Communication Module +/// +/// Transport-agnostic verification and email client. + +pub mod verification; +pub mod email; +pub mod rhai; + +pub use verification::{Verification, VerificationStatus, VerificationTransport}; +pub use email::EmailClient; diff --git a/src/objects/communication/rhai.rs b/src/objects/communication/rhai.rs new file mode 100644 index 0000000..54636cf --- /dev/null +++ b/src/objects/communication/rhai.rs @@ -0,0 +1,180 @@ +/// Rhai bindings for Communication (Verification and Email) + +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; + +use super::verification::{Verification, VerificationStatus, VerificationTransport}; +use super::email::EmailClient; + +// ============================================================================ +// Verification Module +// ============================================================================ + +type RhaiVerification = Verification; + +#[export_module] +mod rhai_verification_module { + use super::RhaiVerification; + use super::super::verification::{Verification, VerificationTransport}; + + #[rhai_fn(name = "new_verification", return_raw)] + pub fn new_verification( + entity_id: String, + contact: String, + ) -> Result> { + // Default to email transport + Ok(Verification::new(0, entity_id, contact, VerificationTransport::Email)) + } + + #[rhai_fn(name = "callback_url", return_raw)] + pub fn set_callback_url( + verification: &mut RhaiVerification, + url: String, + ) -> Result> { + let owned = std::mem::take(verification); + *verification = owned.callback_url(url); + Ok(verification.clone()) + } + + #[rhai_fn(name = "mark_sent", return_raw)] + pub fn mark_sent( + verification: &mut RhaiVerification, + ) -> Result<(), Box> { + verification.mark_sent(); + Ok(()) + } + + #[rhai_fn(name = "verify_code", return_raw)] + pub fn verify_code( + verification: &mut RhaiVerification, + code: String, + ) -> Result<(), Box> { + verification.verify_code(&code) + .map_err(|e| e.into()) + } + + #[rhai_fn(name = "verify_nonce", return_raw)] + pub fn verify_nonce( + verification: &mut RhaiVerification, + nonce: String, + ) -> Result<(), Box> { + verification.verify_nonce(&nonce) + .map_err(|e| e.into()) + } + + #[rhai_fn(name = "resend", return_raw)] + pub fn resend( + verification: &mut RhaiVerification, + ) -> Result<(), Box> { + verification.resend(); + Ok(()) + } + + // Getters + #[rhai_fn(name = "get_entity_id")] + pub fn get_entity_id(verification: &mut RhaiVerification) -> String { + verification.entity_id.clone() + } + + #[rhai_fn(name = "get_contact")] + pub fn get_contact(verification: &mut RhaiVerification) -> String { + verification.contact.clone() + } + + #[rhai_fn(name = "get_code")] + pub fn get_code(verification: &mut RhaiVerification) -> String { + verification.verification_code.clone() + } + + #[rhai_fn(name = "get_nonce")] + pub fn get_nonce(verification: &mut RhaiVerification) -> String { + verification.verification_nonce.clone() + } + + #[rhai_fn(name = "get_verification_url")] + pub fn get_verification_url(verification: &mut RhaiVerification) -> String { + verification.get_verification_url().unwrap_or_default() + } + + #[rhai_fn(name = "get_status")] + pub fn get_status(verification: &mut RhaiVerification) -> String { + format!("{:?}", verification.status) + } + + #[rhai_fn(name = "get_attempts")] + pub fn get_attempts(verification: &mut RhaiVerification) -> i64 { + verification.attempts as i64 + } +} + +// ============================================================================ +// Email Client Module +// ============================================================================ + +type RhaiEmailClient = EmailClient; + +#[export_module] +mod rhai_email_module { + use super::RhaiEmailClient; + use super::super::email::EmailClient; + use super::super::verification::Verification; + use ::rhai::EvalAltResult; + + #[rhai_fn(name = "new_email_client", return_raw)] + pub fn new_email_client() -> Result> { + Ok(EmailClient::new()) + } + + #[rhai_fn(name = "send_verification_code", return_raw)] + pub fn send_verification_code( + client: &mut RhaiEmailClient, + verification: Verification, + ) -> Result<(), Box> { + client.send_verification_code_sync(&verification) + .map_err(|e| e.into()) + } + + #[rhai_fn(name = "send_verification_link", return_raw)] + pub fn send_verification_link( + client: &mut RhaiEmailClient, + verification: Verification, + ) -> Result<(), Box> { + client.send_verification_link_sync(&verification) + .map_err(|e| e.into()) + } +} + +// ============================================================================ +// Registration Functions +// ============================================================================ + +/// Register Communication modules into a Rhai Module +pub fn register_communication_modules(parent_module: &mut Module) { + // Register custom types + parent_module.set_custom_type::("Verification"); + parent_module.set_custom_type::("EmailClient"); + + // Merge verification functions + let verification_module = exported_module!(rhai_verification_module); + parent_module.merge(&verification_module); + + // Merge email client functions + let email_module = exported_module!(rhai_email_module); + parent_module.merge(&email_module); +} + +// ============================================================================ +// CustomType Implementations +// ============================================================================ + +impl CustomType for Verification { + fn build(mut builder: TypeBuilder) { + builder.with_name("Verification"); + } +} + +impl CustomType for EmailClient { + fn build(mut builder: TypeBuilder) { + builder.with_name("EmailClient"); + } +} diff --git a/src/objects/communication/verification.rs b/src/objects/communication/verification.rs new file mode 100644 index 0000000..1de6ffd --- /dev/null +++ b/src/objects/communication/verification.rs @@ -0,0 +1,239 @@ +/// Transport-Agnostic Verification +/// +/// Manages verification sessions with codes and nonces for email, SMS, etc. + +use crate::store::{BaseData, Object, Storable}; +use serde::{Deserialize, Serialize}; + +/// Verification transport type +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum VerificationTransport { + Email, + Sms, + WhatsApp, + Telegram, + Other(String), +} + +impl Default for VerificationTransport { + fn default() -> Self { + VerificationTransport::Email + } +} + +/// Verification status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum VerificationStatus { + #[default] + Pending, + Sent, + Verified, + Expired, + Failed, +} + +/// Verification Session +/// +/// Transport-agnostic verification that can be used for email, SMS, etc. +/// Supports both code-based verification and URL-based (nonce) verification. +#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)] +pub struct Verification { + #[serde(flatten)] + pub base_data: BaseData, + + /// User/entity ID this verification is for + pub entity_id: String, + + /// Contact address (email, phone, etc.) + pub contact: String, + + /// Transport type + pub transport: VerificationTransport, + + /// Verification code (6 digits for user entry) + pub verification_code: String, + + /// Verification nonce (for URL-based verification) + pub verification_nonce: String, + + /// Current status + pub status: VerificationStatus, + + /// When verification was sent + pub sent_at: Option, + + /// When verification was completed + pub verified_at: Option, + + /// When verification expires + pub expires_at: Option, + + /// Number of attempts + pub attempts: u32, + + /// Maximum attempts allowed + pub max_attempts: u32, + + /// Callback URL (for server to construct verification link) + pub callback_url: Option, + + /// Additional metadata + #[serde(default)] + pub metadata: std::collections::HashMap, +} + +impl Verification { + /// Create a new verification + pub fn new(id: u32, entity_id: String, contact: String, transport: VerificationTransport) -> Self { + let mut base_data = BaseData::new(); + base_data.id = id; + + // Generate verification code (6 digits) + let code = Self::generate_code(); + + // Generate verification nonce (32 char hex) + let nonce = Self::generate_nonce(); + + // Set expiry to 24 hours from now + let expires_at = Self::now() + (24 * 60 * 60); + + Self { + base_data, + entity_id, + contact, + transport, + verification_code: code, + verification_nonce: nonce, + status: VerificationStatus::Pending, + sent_at: None, + verified_at: None, + expires_at: Some(expires_at), + attempts: 0, + max_attempts: 3, + callback_url: None, + metadata: std::collections::HashMap::new(), + } + } + + /// Generate a 6-digit verification code + fn generate_code() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + format!("{:06}", (timestamp % 1_000_000) as u32) + } + + /// Generate a verification nonce (32 char hex string) + fn generate_nonce() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + format!("{:032x}", timestamp) + } + + /// Set callback URL + pub fn callback_url(mut self, url: String) -> Self { + self.callback_url = Some(url); + self + } + + /// Get verification URL (callback_url + nonce) + pub fn get_verification_url(&self) -> Option { + self.callback_url.as_ref().map(|base_url| { + if base_url.contains('?') { + format!("{}&nonce={}", base_url, self.verification_nonce) + } else { + format!("{}?nonce={}", base_url, self.verification_nonce) + } + }) + } + + /// Mark as sent + pub fn mark_sent(&mut self) { + self.status = VerificationStatus::Sent; + self.sent_at = Some(Self::now()); + self.base_data.update_modified(); + } + + /// Verify with code + pub fn verify_code(&mut self, code: &str) -> Result<(), String> { + // Check if expired + if let Some(expires_at) = self.expires_at { + if Self::now() > expires_at { + self.status = VerificationStatus::Expired; + self.base_data.update_modified(); + return Err("Verification code expired".to_string()); + } + } + + // Check attempts + self.attempts += 1; + if self.attempts > self.max_attempts { + self.status = VerificationStatus::Failed; + self.base_data.update_modified(); + return Err("Maximum attempts exceeded".to_string()); + } + + // Check code + if code != self.verification_code { + self.base_data.update_modified(); + return Err("Invalid verification code".to_string()); + } + + // Success + self.status = VerificationStatus::Verified; + self.verified_at = Some(Self::now()); + self.base_data.update_modified(); + Ok(()) + } + + /// Verify with nonce (for URL-based verification) + pub fn verify_nonce(&mut self, nonce: &str) -> Result<(), String> { + // Check if expired + if let Some(expires_at) = self.expires_at { + if Self::now() > expires_at { + self.status = VerificationStatus::Expired; + self.base_data.update_modified(); + return Err("Verification link expired".to_string()); + } + } + + // Check nonce + if nonce != self.verification_nonce { + self.base_data.update_modified(); + return Err("Invalid verification link".to_string()); + } + + // Success + self.status = VerificationStatus::Verified; + self.verified_at = Some(Self::now()); + self.base_data.update_modified(); + Ok(()) + } + + /// Resend verification (generate new code and nonce) + pub fn resend(&mut self) { + self.verification_code = Self::generate_code(); + self.verification_nonce = Self::generate_nonce(); + self.status = VerificationStatus::Pending; + self.attempts = 0; + + // Extend expiry + self.expires_at = Some(Self::now() + (24 * 60 * 60)); + self.base_data.update_modified(); + } + + /// Helper to get current timestamp + fn now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + } +} diff --git a/src/objects/communication/verification_old.rs b/src/objects/communication/verification_old.rs new file mode 100644 index 0000000..cb4be5a --- /dev/null +++ b/src/objects/communication/verification_old.rs @@ -0,0 +1,155 @@ +/// Email Verification +/// +/// Manages email verification sessions and status. + +use crate::store::{BaseData, Object, Storable}; +use serde::{Deserialize, Serialize}; + +/// Email verification status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum VerificationStatus { + #[default] + Pending, + Sent, + Verified, + Expired, + Failed, +} + +/// Email Verification Session +#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)] +pub struct EmailVerification { + #[serde(flatten)] + pub base_data: BaseData, + + /// User/entity ID this verification is for + pub entity_id: String, + + /// Email address to verify + pub email: String, + + /// Verification code/token + pub verification_code: String, + + /// Current status + pub status: VerificationStatus, + + /// When verification was sent + pub sent_at: Option, + + /// When verification was completed + pub verified_at: Option, + + /// When verification expires + pub expires_at: Option, + + /// Number of attempts + pub attempts: u32, + + /// Maximum attempts allowed + pub max_attempts: u32, + + /// Additional metadata + #[serde(default)] + pub metadata: std::collections::HashMap, +} + +impl EmailVerification { + /// Create a new email verification + pub fn new(id: u32, entity_id: String, email: String) -> Self { + let mut base_data = BaseData::new(); + base_data.id = id; + + // Generate verification code (6 digits) + let code = Self::generate_code(); + + // Set expiry to 24 hours from now + let expires_at = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + (24 * 60 * 60); + + Self { + base_data, + entity_id, + email, + verification_code: code, + status: VerificationStatus::Pending, + sent_at: None, + verified_at: None, + expires_at: Some(expires_at), + attempts: 0, + max_attempts: 3, + metadata: std::collections::HashMap::new(), + } + } + + /// Generate a 6-digit verification code + fn generate_code() -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + format!("{:06}", (timestamp % 1_000_000) as u32) + } + + /// Mark as sent + pub fn mark_sent(&mut self) { + self.status = VerificationStatus::Sent; + self.sent_at = Some(Self::now()); + self.base_data.update_modified(); + } + + /// Verify with code + pub fn verify(&mut self, code: &str) -> Result<(), String> { + // Check if expired + if let Some(expires_at) = self.expires_at { + if Self::now() > expires_at { + self.status = VerificationStatus::Expired; + self.base_data.update_modified(); + return Err("Verification code expired".to_string()); + } + } + + // Check attempts + self.attempts += 1; + if self.attempts > self.max_attempts { + self.status = VerificationStatus::Failed; + self.base_data.update_modified(); + return Err("Maximum attempts exceeded".to_string()); + } + + // Check code + if code != self.verification_code { + self.base_data.update_modified(); + return Err("Invalid verification code".to_string()); + } + + // Success + self.status = VerificationStatus::Verified; + self.verified_at = Some(Self::now()); + self.base_data.update_modified(); + Ok(()) + } + + /// Resend verification (generate new code) + pub fn resend(&mut self) { + self.verification_code = Self::generate_code(); + self.status = VerificationStatus::Pending; + self.attempts = 0; + + // Extend expiry + self.expires_at = Some(Self::now() + (24 * 60 * 60)); + self.base_data.update_modified(); + } + + /// Helper to get current timestamp + fn now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + } +} diff --git a/src/objects/event/mod.rs b/src/objects/event/mod.rs index 98d3a77..5d8e328 100644 --- a/src/objects/event/mod.rs +++ b/src/objects/event/mod.rs @@ -56,7 +56,7 @@ impl Event { pub fn new(ns: String, title: impl ToString) -> Self { let now = OffsetDateTime::now_utc(); Self { - base_data: BaseData::new(ns), + base_data: BaseData::with_ns(ns), title: title.to_string(), description: None, start_time: now, @@ -71,8 +71,9 @@ impl Event { /// Create an event with specific ID pub fn with_id(id: String, ns: String, title: impl ToString) -> Self { let now = OffsetDateTime::now_utc(); + let id_u32 = id.parse::().unwrap_or(0); Self { - base_data: BaseData::with_id(id, ns), + base_data: BaseData::with_id(id_u32, ns), title: title.to_string(), description: None, start_time: now, diff --git a/src/objects/flow/instance.rs b/src/objects/flow/instance.rs new file mode 100644 index 0000000..fcc74bb --- /dev/null +++ b/src/objects/flow/instance.rs @@ -0,0 +1,241 @@ +/// Flow Instance +/// +/// Represents an active instance of a flow template for a specific entity (e.g., user). + +use crate::store::{BaseData, Object, Storable}; +use serde::{Deserialize, Serialize}; + +/// Status of a step in a flow instance +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum StepStatus { + #[default] + Pending, + Active, + Completed, + Skipped, + Failed, +} + +/// A step instance in a flow +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct StepInstance { + /// Step name (from template) + pub name: String, + + /// Current status + pub status: StepStatus, + + /// When step was started + pub started_at: Option, + + /// When step was completed + pub completed_at: Option, + + /// Step result data + #[serde(default)] + pub result: std::collections::HashMap, + + /// Error message if failed + pub error: Option, +} + +impl StepInstance { + pub fn new(name: String) -> Self { + Self { + name, + status: StepStatus::Pending, + started_at: None, + completed_at: None, + result: std::collections::HashMap::new(), + error: None, + } + } +} + +/// Overall flow status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "lowercase")] +pub enum FlowStatus { + #[default] + Created, + Running, + Completed, + Failed, + Cancelled, +} + +/// Flow Instance - an active execution of a flow template +#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)] +pub struct FlowInstance { + #[serde(flatten)] + pub base_data: BaseData, + + /// Instance name (typically entity_id or unique identifier) + pub name: String, + + /// Template name this instance is based on + pub template_name: String, + + /// Entity ID this flow is for (e.g., user_id) + pub entity_id: String, + + /// Current flow status + pub status: FlowStatus, + + /// Step instances + pub steps: Vec, + + /// Current step index + pub current_step: usize, + + /// When flow was started + pub started_at: Option, + + /// When flow was completed + pub completed_at: Option, + + /// Instance metadata + #[serde(default)] + pub metadata: std::collections::HashMap, +} + +impl FlowInstance { + /// Create a new flow instance + pub fn new(id: u32, name: String, template_name: String, entity_id: String) -> Self { + let mut base_data = BaseData::new(); + base_data.id = id; + Self { + base_data, + name, + template_name, + entity_id, + status: FlowStatus::Created, + steps: Vec::new(), + current_step: 0, + started_at: None, + completed_at: None, + metadata: std::collections::HashMap::new(), + } + } + + /// Initialize steps from template + pub fn init_steps(&mut self, step_names: Vec) { + self.steps = step_names.into_iter().map(StepInstance::new).collect(); + self.base_data.update_modified(); + } + + /// Start the flow + pub fn start(&mut self) { + // Initialize default steps if none exist + if self.steps.is_empty() { + // Create default steps based on common workflow + self.steps = vec![ + StepInstance::new("registration".to_string()), + StepInstance::new("kyc".to_string()), + StepInstance::new("email".to_string()), + ]; + } + + self.status = FlowStatus::Running; + self.started_at = Some(Self::now()); + + // Start first step if exists + if let Some(step) = self.steps.first_mut() { + step.status = StepStatus::Active; + step.started_at = Some(Self::now()); + } + + self.base_data.update_modified(); + } + + /// Complete a step by name + pub fn complete_step(&mut self, step_name: &str) -> Result<(), String> { + let step_idx = self.steps.iter().position(|s| s.name == step_name) + .ok_or_else(|| format!("Step '{}' not found", step_name))?; + + let step = &mut self.steps[step_idx]; + step.status = StepStatus::Completed; + step.completed_at = Some(Self::now()); + + // Move to next step if this was the current step + if step_idx == self.current_step { + self.current_step += 1; + + // Start next step if exists + if let Some(next_step) = self.steps.get_mut(self.current_step) { + next_step.status = StepStatus::Active; + next_step.started_at = Some(Self::now()); + } else { + // All steps completed + self.status = FlowStatus::Completed; + self.completed_at = Some(Self::now()); + } + } + + self.base_data.update_modified(); + Ok(()) + } + + /// Fail a step + pub fn fail_step(&mut self, step_name: &str, error: String) -> Result<(), String> { + let step = self.steps.iter_mut() + .find(|s| s.name == step_name) + .ok_or_else(|| format!("Step '{}' not found", step_name))?; + + step.status = StepStatus::Failed; + step.error = Some(error); + step.completed_at = Some(Self::now()); + + self.status = FlowStatus::Failed; + self.base_data.update_modified(); + Ok(()) + } + + /// Skip a step + pub fn skip_step(&mut self, step_name: &str) -> Result<(), String> { + let step = self.steps.iter_mut() + .find(|s| s.name == step_name) + .ok_or_else(|| format!("Step '{}' not found", step_name))?; + + step.status = StepStatus::Skipped; + step.completed_at = Some(Self::now()); + self.base_data.update_modified(); + Ok(()) + } + + /// Get current step + pub fn get_current_step(&self) -> Option<&StepInstance> { + self.steps.get(self.current_step) + } + + /// Get step by name + pub fn get_step(&self, name: &str) -> Option<&StepInstance> { + self.steps.iter().find(|s| s.name == name) + } + + /// Set step result data + pub fn set_step_result(&mut self, step_name: &str, key: String, value: String) -> Result<(), String> { + let step = self.steps.iter_mut() + .find(|s| s.name == step_name) + .ok_or_else(|| format!("Step '{}' not found", step_name))?; + + step.result.insert(key, value); + self.base_data.update_modified(); + Ok(()) + } + + /// Add metadata + pub fn add_metadata(&mut self, key: String, value: String) { + self.metadata.insert(key, value); + self.base_data.update_modified(); + } + + /// Helper to get current timestamp + fn now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() + } +} diff --git a/src/objects/flow/mod.rs b/src/objects/flow/mod.rs new file mode 100644 index 0000000..313079b --- /dev/null +++ b/src/objects/flow/mod.rs @@ -0,0 +1,10 @@ +/// Flow Module +/// +/// Provides workflow/flow management with templates and instances. + +pub mod template; +pub mod instance; +pub mod rhai; + +pub use template::{FlowTemplate, FlowStep}; +pub use instance::{FlowInstance, FlowStatus, StepStatus, StepInstance}; diff --git a/src/objects/flow/rhai.rs b/src/objects/flow/rhai.rs new file mode 100644 index 0000000..cf5f163 --- /dev/null +++ b/src/objects/flow/rhai.rs @@ -0,0 +1,183 @@ +/// Rhai bindings for Flow objects + +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; + +use super::template::{FlowTemplate, FlowStep}; +use super::instance::{FlowInstance, FlowStatus, StepStatus}; + +// ============================================================================ +// Flow Template Module +// ============================================================================ + +type RhaiFlowTemplate = FlowTemplate; + +#[export_module] +mod rhai_flow_template_module { + use super::RhaiFlowTemplate; + + #[rhai_fn(name = "new_flow", return_raw)] + pub fn new_flow() -> Result> { + Ok(FlowTemplate::new(0)) + } + + #[rhai_fn(name = "name", return_raw)] + pub fn set_name( + template: &mut RhaiFlowTemplate, + name: String, + ) -> Result> { + let owned = std::mem::take(template); + *template = owned.name(name); + Ok(template.clone()) + } + + #[rhai_fn(name = "description", return_raw)] + pub fn set_description( + template: &mut RhaiFlowTemplate, + description: String, + ) -> Result> { + let owned = std::mem::take(template); + *template = owned.description(description); + Ok(template.clone()) + } + + #[rhai_fn(name = "add_step", return_raw)] + pub fn add_step( + template: &mut RhaiFlowTemplate, + name: String, + description: String, + ) -> Result<(), Box> { + template.add_step(name, description); + Ok(()) + } + + #[rhai_fn(name = "build", return_raw)] + pub fn build( + template: &mut RhaiFlowTemplate, + ) -> Result> { + Ok(template.clone()) + } + + // Getters + #[rhai_fn(name = "get_name")] + pub fn get_name(template: &mut RhaiFlowTemplate) -> String { + template.name.clone() + } + + #[rhai_fn(name = "get_description")] + pub fn get_description(template: &mut RhaiFlowTemplate) -> String { + template.description.clone() + } +} + +// ============================================================================ +// Flow Instance Module +// ============================================================================ + +type RhaiFlowInstance = FlowInstance; + +#[export_module] +mod rhai_flow_instance_module { + use super::RhaiFlowInstance; + + #[rhai_fn(name = "new_flow_instance", return_raw)] + pub fn new_instance( + name: String, + template_name: String, + entity_id: String, + ) -> Result> { + Ok(FlowInstance::new(0, name, template_name, entity_id)) + } + + #[rhai_fn(name = "start", return_raw)] + pub fn start( + instance: &mut RhaiFlowInstance, + ) -> Result<(), Box> { + instance.start(); + Ok(()) + } + + #[rhai_fn(name = "complete_step", return_raw)] + pub fn complete_step( + instance: &mut RhaiFlowInstance, + step_name: String, + ) -> Result<(), Box> { + instance.complete_step(&step_name) + .map_err(|e| e.into()) + } + + #[rhai_fn(name = "fail_step", return_raw)] + pub fn fail_step( + instance: &mut RhaiFlowInstance, + step_name: String, + error: String, + ) -> Result<(), Box> { + instance.fail_step(&step_name, error) + .map_err(|e| e.into()) + } + + #[rhai_fn(name = "skip_step", return_raw)] + pub fn skip_step( + instance: &mut RhaiFlowInstance, + step_name: String, + ) -> Result<(), Box> { + instance.skip_step(&step_name) + .map_err(|e| e.into()) + } + + // Getters + #[rhai_fn(name = "get_name")] + pub fn get_name(instance: &mut RhaiFlowInstance) -> String { + instance.name.clone() + } + + #[rhai_fn(name = "get_template_name")] + pub fn get_template_name(instance: &mut RhaiFlowInstance) -> String { + instance.template_name.clone() + } + + #[rhai_fn(name = "get_entity_id")] + pub fn get_entity_id(instance: &mut RhaiFlowInstance) -> String { + instance.entity_id.clone() + } + + #[rhai_fn(name = "get_status")] + pub fn get_status(instance: &mut RhaiFlowInstance) -> String { + format!("{:?}", instance.status) + } +} + +// ============================================================================ +// Registration Functions +// ============================================================================ + +/// Register Flow modules into a Rhai Module (for use in packages) +pub fn register_flow_modules(parent_module: &mut Module) { + // Register custom types + parent_module.set_custom_type::("FlowTemplate"); + parent_module.set_custom_type::("FlowInstance"); + + // Merge flow template functions + let template_module = exported_module!(rhai_flow_template_module); + parent_module.merge(&template_module); + + // Merge flow instance functions + let instance_module = exported_module!(rhai_flow_instance_module); + parent_module.merge(&instance_module); +} + +// ============================================================================ +// CustomType Implementations +// ============================================================================ + +impl CustomType for FlowTemplate { + fn build(mut builder: TypeBuilder) { + builder.with_name("FlowTemplate"); + } +} + +impl CustomType for FlowInstance { + fn build(mut builder: TypeBuilder) { + builder.with_name("FlowInstance"); + } +} diff --git a/src/objects/flow/template.rs b/src/objects/flow/template.rs new file mode 100644 index 0000000..9259664 --- /dev/null +++ b/src/objects/flow/template.rs @@ -0,0 +1,117 @@ +/// Flow Template +/// +/// Defines a reusable workflow template with steps that can be instantiated multiple times. + +use crate::store::{BaseData, Object, Storable}; +use serde::{Deserialize, Serialize}; + +/// A step in a flow template +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct FlowStep { + /// Step name/identifier + pub name: String, + + /// Step description + pub description: String, + + /// Steps that must be completed before this step can start + #[serde(default)] + pub dependencies: Vec, +} + +impl FlowStep { + pub fn new(name: String, description: String) -> Self { + Self { + name, + description, + dependencies: Vec::new(), + } + } + + pub fn with_dependencies(mut self, dependencies: Vec) -> Self { + self.dependencies = dependencies; + self + } + + pub fn add_dependency(&mut self, dependency: String) { + self.dependencies.push(dependency); + } +} + +/// Flow Template - defines a reusable workflow +#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)] +pub struct FlowTemplate { + #[serde(flatten)] + pub base_data: BaseData, + + /// Template name + pub name: String, + + /// Template description + pub description: String, + + /// Ordered list of steps + pub steps: Vec, + + /// Template metadata + #[serde(default)] + pub metadata: std::collections::HashMap, +} + +impl FlowTemplate { + /// Create a new flow template + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + base_data.id = id; + Self { + base_data, + name: String::new(), + description: String::new(), + steps: Vec::new(), + metadata: std::collections::HashMap::new(), + } + } + + /// Builder: Set name + pub fn name(mut self, name: String) -> Self { + self.name = name; + self.base_data.update_modified(); + self + } + + /// Builder: Set description + pub fn description(mut self, description: String) -> Self { + self.description = description; + self.base_data.update_modified(); + self + } + + /// Add a step to the template + pub fn add_step(&mut self, name: String, description: String) { + self.steps.push(FlowStep::new(name, description)); + self.base_data.update_modified(); + } + + /// Add a step with dependencies + pub fn add_step_with_dependencies(&mut self, name: String, description: String, dependencies: Vec) { + let step = FlowStep::new(name, description).with_dependencies(dependencies); + self.steps.push(step); + self.base_data.update_modified(); + } + + /// Get step by name + pub fn get_step(&self, name: &str) -> Option<&FlowStep> { + self.steps.iter().find(|s| s.name == name) + } + + /// Add metadata + pub fn add_metadata(&mut self, key: String, value: String) { + self.metadata.insert(key, value); + self.base_data.update_modified(); + } + + /// Build (for fluent API compatibility) + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/grid4/bid.rs b/src/objects/grid4/bid.rs new file mode 100644 index 0000000..6b8fc1d --- /dev/null +++ b/src/objects/grid4/bid.rs @@ -0,0 +1,126 @@ +use crate::store::BaseData; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; + +/// Bid status enumeration +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub enum BidStatus { + #[default] + Pending, + Confirmed, + Assigned, + Cancelled, + Done, +} + +/// Billing period enumeration +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub enum BillingPeriod { + #[default] + Hourly, + Monthly, + Yearly, + Biannually, + Triannually, +} + +/// I can bid for infra, and optionally get accepted +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Bid { + pub base_data: BaseData, + /// links back to customer for this capacity (user on ledger) + #[index] + pub customer_id: u32, + /// nr of slices I need in 1 machine + pub compute_slices_nr: i32, + /// price per 1 GB slice I want to accept + pub compute_slice_price: f64, + /// nr of storage slices needed + pub storage_slices_nr: i32, + /// price per 1 GB storage slice I want to accept + pub storage_slice_price: f64, + pub status: BidStatus, + /// if obligation then will be charged and money needs to be in escrow, otherwise its an intent + pub obligation: bool, + /// epoch timestamp + pub start_date: u32, + /// epoch timestamp + pub end_date: u32, + /// signature as done by a user/consumer to validate their identity and intent + pub signature_user: String, + pub billing_period: BillingPeriod, +} + +impl Bid { + pub fn new() -> Self { + Self { + base_data: BaseData::new(), + customer_id: 0, + compute_slices_nr: 0, + compute_slice_price: 0.0, + storage_slices_nr: 0, + storage_slice_price: 0.0, + status: BidStatus::default(), + obligation: false, + start_date: 0, + end_date: 0, + signature_user: String::new(), + billing_period: BillingPeriod::default(), + } + } + + pub fn customer_id(mut self, v: u32) -> Self { + self.customer_id = v; + self + } + + pub fn compute_slices_nr(mut self, v: i32) -> Self { + self.compute_slices_nr = v; + self + } + + pub fn compute_slice_price(mut self, v: f64) -> Self { + self.compute_slice_price = v; + self + } + + pub fn storage_slices_nr(mut self, v: i32) -> Self { + self.storage_slices_nr = v; + self + } + + pub fn storage_slice_price(mut self, v: f64) -> Self { + self.storage_slice_price = v; + self + } + + pub fn status(mut self, v: BidStatus) -> Self { + self.status = v; + self + } + + pub fn obligation(mut self, v: bool) -> Self { + self.obligation = v; + self + } + + pub fn start_date(mut self, v: u32) -> Self { + self.start_date = v; + self + } + + pub fn end_date(mut self, v: u32) -> Self { + self.end_date = v; + self + } + + pub fn signature_user(mut self, v: impl ToString) -> Self { + self.signature_user = v.to_string(); + self + } + + pub fn billing_period(mut self, v: BillingPeriod) -> Self { + self.billing_period = v; + self + } +} diff --git a/src/objects/grid4/common.rs b/src/objects/grid4/common.rs new file mode 100644 index 0000000..2014aff --- /dev/null +++ b/src/objects/grid4/common.rs @@ -0,0 +1,39 @@ +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; + +/// SLA policy matching the V spec `SLAPolicy` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct SLAPolicy { + /// should +90 + pub sla_uptime: i32, + /// minimal mbits we can expect avg over 1h per node, 0 means we don't guarantee + pub sla_bandwidth_mbit: i32, + /// 0-100, percent of money given back in relation to month if sla breached, + /// e.g. 200 means we return 2 months worth of rev if sla missed + pub sla_penalty: i32, +} + +impl SLAPolicy { + pub fn new() -> Self { Self::default() } + pub fn sla_uptime(mut self, v: i32) -> Self { self.sla_uptime = v; self } + pub fn sla_bandwidth_mbit(mut self, v: i32) -> Self { self.sla_bandwidth_mbit = v; self } + pub fn sla_penalty(mut self, v: i32) -> Self { self.sla_penalty = v; self } + pub fn build(self) -> Self { self } +} + +/// Pricing policy matching the V spec `PricingPolicy` +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct PricingPolicy { + /// e.g. 30,40,50 means if user has more CC in wallet than 1 year utilization + /// then this provider gives 30%, 2Y 40%, ... + pub marketplace_year_discounts: Vec, + /// e.g. 10,20,30 + pub volume_discounts: Vec, +} + +impl PricingPolicy { + pub fn new() -> Self { Self { marketplace_year_discounts: vec![30, 40, 50], volume_discounts: vec![10, 20, 30] } } + pub fn marketplace_year_discounts(mut self, v: Vec) -> Self { self.marketplace_year_discounts = v; self } + pub fn volume_discounts(mut self, v: Vec) -> Self { self.volume_discounts = v; self } + pub fn build(self) -> Self { self } +} diff --git a/src/objects/grid4/contract.rs b/src/objects/grid4/contract.rs new file mode 100644 index 0000000..98f2439 --- /dev/null +++ b/src/objects/grid4/contract.rs @@ -0,0 +1,217 @@ +use crate::store::BaseData; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; +use super::bid::BillingPeriod; + +/// Contract status enumeration +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub enum ContractStatus { + #[default] + Active, + Cancelled, + Error, + Paused, +} + +/// Compute slice provisioned for a contract +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct ComputeSliceProvisioned { + pub node_id: u32, + /// the id of the slice in the node + pub id: u16, + pub mem_gb: f64, + pub storage_gb: f64, + pub passmark: i32, + pub vcores: i32, + pub cpu_oversubscription: i32, + pub tags: String, +} + +/// Storage slice provisioned for a contract +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct StorageSliceProvisioned { + pub node_id: u32, + /// the id of the slice in the node, are tracked in the node itself + pub id: u16, + pub storage_size_gb: i32, + pub tags: String, +} + +/// Contract for provisioned infrastructure +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Contract { + pub base_data: BaseData, + /// links back to customer for this capacity (user on ledger) + #[index] + pub customer_id: u32, + pub compute_slices: Vec, + pub storage_slices: Vec, + /// price per 1 GB agreed upon + pub compute_slice_price: f64, + /// price per 1 GB agreed upon + pub storage_slice_price: f64, + /// price per 1 GB agreed upon (transfer) + pub network_slice_price: f64, + pub status: ContractStatus, + /// epoch timestamp + pub start_date: u32, + /// epoch timestamp + pub end_date: u32, + /// signature as done by a user/consumer to validate their identity and intent + pub signature_user: String, + /// signature as done by the hoster + pub signature_hoster: String, + pub billing_period: BillingPeriod, +} + +impl Contract { + pub fn new() -> Self { + Self { + base_data: BaseData::new(), + customer_id: 0, + compute_slices: Vec::new(), + storage_slices: Vec::new(), + compute_slice_price: 0.0, + storage_slice_price: 0.0, + network_slice_price: 0.0, + status: ContractStatus::default(), + start_date: 0, + end_date: 0, + signature_user: String::new(), + signature_hoster: String::new(), + billing_period: BillingPeriod::default(), + } + } + + pub fn customer_id(mut self, v: u32) -> Self { + self.customer_id = v; + self + } + + pub fn add_compute_slice(mut self, slice: ComputeSliceProvisioned) -> Self { + self.compute_slices.push(slice); + self + } + + pub fn add_storage_slice(mut self, slice: StorageSliceProvisioned) -> Self { + self.storage_slices.push(slice); + self + } + + pub fn compute_slice_price(mut self, v: f64) -> Self { + self.compute_slice_price = v; + self + } + + pub fn storage_slice_price(mut self, v: f64) -> Self { + self.storage_slice_price = v; + self + } + + pub fn network_slice_price(mut self, v: f64) -> Self { + self.network_slice_price = v; + self + } + + pub fn status(mut self, v: ContractStatus) -> Self { + self.status = v; + self + } + + pub fn start_date(mut self, v: u32) -> Self { + self.start_date = v; + self + } + + pub fn end_date(mut self, v: u32) -> Self { + self.end_date = v; + self + } + + pub fn signature_user(mut self, v: impl ToString) -> Self { + self.signature_user = v.to_string(); + self + } + + pub fn signature_hoster(mut self, v: impl ToString) -> Self { + self.signature_hoster = v.to_string(); + self + } + + pub fn billing_period(mut self, v: BillingPeriod) -> Self { + self.billing_period = v; + self + } +} + +impl ComputeSliceProvisioned { + pub fn new() -> Self { + Self::default() + } + + pub fn node_id(mut self, v: u32) -> Self { + self.node_id = v; + self + } + + pub fn id(mut self, v: u16) -> Self { + self.id = v; + self + } + + pub fn mem_gb(mut self, v: f64) -> Self { + self.mem_gb = v; + self + } + + pub fn storage_gb(mut self, v: f64) -> Self { + self.storage_gb = v; + self + } + + pub fn passmark(mut self, v: i32) -> Self { + self.passmark = v; + self + } + + pub fn vcores(mut self, v: i32) -> Self { + self.vcores = v; + self + } + + pub fn cpu_oversubscription(mut self, v: i32) -> Self { + self.cpu_oversubscription = v; + self + } + + pub fn tags(mut self, v: impl ToString) -> Self { + self.tags = v.to_string(); + self + } +} + +impl StorageSliceProvisioned { + pub fn new() -> Self { + Self::default() + } + + pub fn node_id(mut self, v: u32) -> Self { + self.node_id = v; + self + } + + pub fn id(mut self, v: u16) -> Self { + self.id = v; + self + } + + pub fn storage_size_gb(mut self, v: i32) -> Self { + self.storage_size_gb = v; + self + } + + pub fn tags(mut self, v: impl ToString) -> Self { + self.tags = v.to_string(); + self + } +} diff --git a/src/objects/grid4/mod.rs b/src/objects/grid4/mod.rs new file mode 100644 index 0000000..f5d808c --- /dev/null +++ b/src/objects/grid4/mod.rs @@ -0,0 +1,18 @@ +pub mod bid; +pub mod common; +pub mod contract; +pub mod node; +pub mod nodegroup; +pub mod reputation; +pub mod reservation; + +pub use bid::{Bid, BidStatus, BillingPeriod}; +pub use common::{PricingPolicy, SLAPolicy}; +pub use contract::{Contract, ContractStatus, ComputeSliceProvisioned, StorageSliceProvisioned}; +pub use node::{ + CPUDevice, ComputeSlice, DeviceInfo, GPUDevice, MemoryDevice, NetworkDevice, Node, + NodeCapacity, StorageDevice, StorageSlice, +}; +pub use nodegroup::NodeGroup; +pub use reputation::{NodeGroupReputation, NodeReputation}; +pub use reservation::{Reservation, ReservationStatus}; diff --git a/src/objects/grid4/node.rs b/src/objects/grid4/node.rs new file mode 100644 index 0000000..b079b99 --- /dev/null +++ b/src/objects/grid4/node.rs @@ -0,0 +1,279 @@ +use crate::store::BaseData; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; +use super::common::{PricingPolicy, SLAPolicy}; + +/// Storage device information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct StorageDevice { + /// can be used in node + pub id: String, + /// Size of the storage device in gigabytes + pub size_gb: f64, + /// Description of the storage device + pub description: String, +} + +/// Memory device information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct MemoryDevice { + /// can be used in node + pub id: String, + /// Size of the memory device in gigabytes + pub size_gb: f64, + /// Description of the memory device + pub description: String, +} + +/// CPU device information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct CPUDevice { + /// can be used in node + pub id: String, + /// Number of CPU cores + pub cores: i32, + /// Passmark score + pub passmark: i32, + /// Description of the CPU + pub description: String, + /// Brand of the CPU + pub cpu_brand: String, + /// Version of the CPU + pub cpu_version: String, +} + +/// GPU device information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct GPUDevice { + /// can be used in node + pub id: String, + /// Number of GPU cores + pub cores: i32, + /// Size of the GPU memory in gigabytes + pub memory_gb: f64, + /// Description of the GPU + pub description: String, + pub gpu_brand: String, + pub gpu_version: String, +} + +/// Network device information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct NetworkDevice { + /// can be used in node + pub id: String, + /// Network speed in Mbps + pub speed_mbps: i32, + /// Description of the network device + pub description: String, +} + +/// Aggregated device info for a node +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct DeviceInfo { + pub vendor: String, + pub storage: Vec, + pub memory: Vec, + pub cpu: Vec, + pub gpu: Vec, + pub network: Vec, +} + +/// NodeCapacity represents the hardware capacity details of a node. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct NodeCapacity { + /// Total storage in gigabytes + pub storage_gb: f64, + /// Total memory in gigabytes + pub mem_gb: f64, + /// Total GPU memory in gigabytes + pub mem_gb_gpu: f64, + /// Passmark score for the node + pub passmark: i32, + /// Total virtual cores + pub vcores: i32, +} + +// PricingPolicy and SLAPolicy moved to `common.rs` to be shared across models. + +/// Compute slice (typically represents a base unit of compute) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct ComputeSlice { + /// the id of the slice in the node + pub id: u16, + pub mem_gb: f64, + pub storage_gb: f64, + pub passmark: i32, + pub vcores: i32, + pub cpu_oversubscription: i32, + pub storage_oversubscription: i32, + /// nr of GPU's see node to know what GPU's are + pub gpus: u8, +} + +impl ComputeSlice { + pub fn new() -> Self { + Self { + id: 0, + mem_gb: 0.0, + storage_gb: 0.0, + passmark: 0, + vcores: 0, + cpu_oversubscription: 0, + storage_oversubscription: 0, + gpus: 0, + } + } + + pub fn id(mut self, id: u16) -> Self { + self.id = id; + self + } + pub fn mem_gb(mut self, v: f64) -> Self { + self.mem_gb = v; + self + } + pub fn storage_gb(mut self, v: f64) -> Self { + self.storage_gb = v; + self + } + pub fn passmark(mut self, v: i32) -> Self { + self.passmark = v; + self + } + pub fn vcores(mut self, v: i32) -> Self { + self.vcores = v; + self + } + pub fn cpu_oversubscription(mut self, v: i32) -> Self { + self.cpu_oversubscription = v; + self + } + pub fn storage_oversubscription(mut self, v: i32) -> Self { + self.storage_oversubscription = v; + self + } + pub fn gpus(mut self, v: u8) -> Self { + self.gpus = v; + self + } +} + +/// Storage slice (typically 1GB of storage) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct StorageSlice { + /// the id of the slice in the node, are tracked in the node itself + pub id: u16, +} + +impl StorageSlice { + pub fn new() -> Self { + Self { + id: 0, + } + } + + pub fn id(mut self, id: u16) -> Self { + self.id = id; + self + } +} + +/// Grid4 Node model +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Node { + pub base_data: BaseData, + /// Link to node group + #[index] + pub nodegroupid: i32, + /// Uptime percentage 0..100 + pub uptime: i32, + pub computeslices: Vec, + pub storageslices: Vec, + pub devices: DeviceInfo, + /// 2 letter code as specified in lib/data/countries/data/countryInfo.txt + #[index] + pub country: String, + /// Hardware capacity details + pub capacity: NodeCapacity, + /// first time node was active + pub birthtime: u32, + /// node public key + #[index] + pub pubkey: String, + /// signature done on node to validate pubkey with privkey + pub signature_node: String, + /// signature as done by farmers to validate their identity + pub signature_farmer: String, +} + +impl Node { + pub fn new() -> Self { + Self { + base_data: BaseData::new(), + nodegroupid: 0, + uptime: 0, + computeslices: Vec::new(), + storageslices: Vec::new(), + devices: DeviceInfo::default(), + country: String::new(), + capacity: NodeCapacity::default(), + birthtime: 0, + pubkey: String::new(), + signature_node: String::new(), + signature_farmer: String::new(), + } + } + + pub fn nodegroupid(mut self, v: i32) -> Self { + self.nodegroupid = v; + self + } + pub fn uptime(mut self, v: i32) -> Self { + self.uptime = v; + self + } + pub fn add_compute_slice(mut self, s: ComputeSlice) -> Self { + self.computeslices.push(s); + self + } + pub fn add_storage_slice(mut self, s: StorageSlice) -> Self { + self.storageslices.push(s); + self + } + pub fn devices(mut self, d: DeviceInfo) -> Self { + self.devices = d; + self + } + pub fn country(mut self, c: impl ToString) -> Self { + self.country = c.to_string(); + self + } + pub fn capacity(mut self, c: NodeCapacity) -> Self { + self.capacity = c; + self + } + pub fn birthtime(mut self, t: u32) -> Self { + self.birthtime = t; + self + } + + pub fn pubkey(mut self, v: impl ToString) -> Self { + self.pubkey = v.to_string(); + self + } + pub fn signature_node(mut self, v: impl ToString) -> Self { + self.signature_node = v.to_string(); + self + } + pub fn signature_farmer(mut self, v: impl ToString) -> Self { + self.signature_farmer = v.to_string(); + self + } + + /// Placeholder for capacity recalculation out of the devices on the Node + pub fn check(self) -> Self { + // TODO: calculate NodeCapacity out of the devices on the Node + self + } +} diff --git a/src/objects/grid4/nodegroup.rs b/src/objects/grid4/nodegroup.rs new file mode 100644 index 0000000..96789c3 --- /dev/null +++ b/src/objects/grid4/nodegroup.rs @@ -0,0 +1,50 @@ +use crate::store::BaseData; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; + +use super::common::{PricingPolicy, SLAPolicy}; + +/// Grid4 NodeGroup model (root object for farmer configuration) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct NodeGroup { + pub base_data: BaseData, + /// link back to farmer who owns the nodegroup, is a user? + #[index] + pub farmerid: u32, + /// only visible by farmer, in future encrypted, used to boot a node + pub secret: String, + pub description: String, + pub slapolicy: SLAPolicy, + pub pricingpolicy: PricingPolicy, + /// pricing in CC - cloud credit, per 2GB node slice + pub compute_slice_normalized_pricing_cc: f64, + /// pricing in CC - cloud credit, per 1GB storage slice + pub storage_slice_normalized_pricing_cc: f64, + /// signature as done by farmers to validate that they created this group + pub signature_farmer: String, +} + +impl NodeGroup { + pub fn new() -> Self { + Self { + base_data: BaseData::new(), + farmerid: 0, + secret: String::new(), + description: String::new(), + slapolicy: SLAPolicy::default(), + pricingpolicy: PricingPolicy::new(), + compute_slice_normalized_pricing_cc: 0.0, + storage_slice_normalized_pricing_cc: 0.0, + signature_farmer: String::new(), + } + } + + pub fn farmerid(mut self, v: u32) -> Self { self.farmerid = v; self } + pub fn secret(mut self, v: impl ToString) -> Self { self.secret = v.to_string(); self } + pub fn description(mut self, v: impl ToString) -> Self { self.description = v.to_string(); self } + pub fn slapolicy(mut self, v: SLAPolicy) -> Self { self.slapolicy = v; self } + pub fn pricingpolicy(mut self, v: PricingPolicy) -> Self { self.pricingpolicy = v; self } + pub fn compute_slice_normalized_pricing_cc(mut self, v: f64) -> Self { self.compute_slice_normalized_pricing_cc = v; self } + pub fn storage_slice_normalized_pricing_cc(mut self, v: f64) -> Self { self.storage_slice_normalized_pricing_cc = v; self } + pub fn signature_farmer(mut self, v: impl ToString) -> Self { self.signature_farmer = v.to_string(); self } +} diff --git a/src/objects/grid4/reputation.rs b/src/objects/grid4/reputation.rs new file mode 100644 index 0000000..d6242c9 --- /dev/null +++ b/src/objects/grid4/reputation.rs @@ -0,0 +1,83 @@ +use crate::store::BaseData; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; + +/// Node reputation information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct NodeReputation { + pub node_id: u32, + /// between 0 and 100, earned over time + pub reputation: i32, + /// between 0 and 100, set by system, farmer has no ability to set this + pub uptime: i32, +} + +/// NodeGroup reputation model +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct NodeGroupReputation { + pub base_data: BaseData, + #[index] + pub nodegroup_id: u32, + /// between 0 and 100, earned over time + pub reputation: i32, + /// between 0 and 100, set by system, farmer has no ability to set this + pub uptime: i32, + pub nodes: Vec, +} + +impl NodeGroupReputation { + pub fn new() -> Self { + Self { + base_data: BaseData::new(), + nodegroup_id: 0, + reputation: 50, // default as per spec + uptime: 0, + nodes: Vec::new(), + } + } + + pub fn nodegroup_id(mut self, v: u32) -> Self { + self.nodegroup_id = v; + self + } + + pub fn reputation(mut self, v: i32) -> Self { + self.reputation = v; + self + } + + pub fn uptime(mut self, v: i32) -> Self { + self.uptime = v; + self + } + + pub fn add_node_reputation(mut self, node_rep: NodeReputation) -> Self { + self.nodes.push(node_rep); + self + } +} + +impl NodeReputation { + pub fn new() -> Self { + Self { + node_id: 0, + reputation: 50, // default as per spec + uptime: 0, + } + } + + pub fn node_id(mut self, v: u32) -> Self { + self.node_id = v; + self + } + + pub fn reputation(mut self, v: i32) -> Self { + self.reputation = v; + self + } + + pub fn uptime(mut self, v: i32) -> Self { + self.uptime = v; + self + } +} diff --git a/src/objects/grid4/reservation.rs b/src/objects/grid4/reservation.rs new file mode 100644 index 0000000..2db13f1 --- /dev/null +++ b/src/objects/grid4/reservation.rs @@ -0,0 +1,56 @@ +use crate::store::BaseData; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; + +/// Reservation status as per V spec +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub enum ReservationStatus { + #[default] + Pending, + Confirmed, + Assigned, + Cancelled, + Done, +} + +/// Grid4 Reservation model +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Reservation { + pub base_data: BaseData, + /// links back to customer for this capacity + #[index] + pub customer_id: u32, + pub compute_slices: Vec, + pub storage_slices: Vec, + pub status: ReservationStatus, + /// if obligation then will be charged and money needs to be in escrow, otherwise its an intent + pub obligation: bool, + /// epoch + pub start_date: u32, + pub end_date: u32, +} + +impl Reservation { + pub fn new() -> Self { + Self { + base_data: BaseData::new(), + customer_id: 0, + compute_slices: Vec::new(), + storage_slices: Vec::new(), + status: ReservationStatus::Pending, + obligation: false, + start_date: 0, + end_date: 0, + } + } + + pub fn customer_id(mut self, v: u32) -> Self { self.customer_id = v; self } + pub fn add_compute_slice(mut self, id: u32) -> Self { self.compute_slices.push(id); self } + pub fn compute_slices(mut self, v: Vec) -> Self { self.compute_slices = v; self } + pub fn add_storage_slice(mut self, id: u32) -> Self { self.storage_slices.push(id); self } + pub fn storage_slices(mut self, v: Vec) -> Self { self.storage_slices = v; self } + pub fn status(mut self, v: ReservationStatus) -> Self { self.status = v; self } + pub fn obligation(mut self, v: bool) -> Self { self.obligation = v; self } + pub fn start_date(mut self, v: u32) -> Self { self.start_date = v; self } + pub fn end_date(mut self, v: u32) -> Self { self.end_date = v; self } +} diff --git a/src/objects/grid4/specs/README.md b/src/objects/grid4/specs/README.md new file mode 100644 index 0000000..5aa351e --- /dev/null +++ b/src/objects/grid4/specs/README.md @@ -0,0 +1,194 @@ + +# Grid4 Data Model + +This module defines data models for nodes, groups, and slices in a cloud/grid infrastructure. Each root object is marked with `@[heap]` and can be indexed for efficient querying. + +## Root Objects Overview + +| Object | Description | Index Fields | +| ----------- | --------------------------------------------- | ------------------------------ | +| `Node` | Represents a single node in the grid | `id`, `nodegroupid`, `country` | +| `NodeGroup` | Represents a group of nodes owned by a farmer | `id`, `farmerid` | + +--- + +## Node + +Represents a single node in the grid with slices, devices, and capacity. + +| Field | Type | Description | Indexed | +| --------------- | ---------------- | -------------------------------------------- | ------- | +| `id` | `int` | Unique node ID | ✅ | +| `nodegroupid` | `int` | ID of the owning node group | ✅ | +| `uptime` | `int` | Uptime percentage (0-100) | ✅ | +| `computeslices` | `[]ComputeSlice` | List of compute slices | ❌ | +| `storageslices` | `[]StorageSlice` | List of storage slices | ❌ | +| `devices` | `DeviceInfo` | Hardware device info (storage, memory, etc.) | ❌ | +| `country` | `string` | 2-letter country code | ✅ | +| `capacity` | `NodeCapacity` | Aggregated hardware capacity | ❌ | +| `provisiontime` | `u32` | Provisioning time (simple/compatible format) | ✅ | + +--- + +## NodeGroup + +Represents a group of nodes owned by a farmer, with policies. + +| Field | Type | Description | Indexed | +| ------------------------------------- | --------------- | ---------------------------------------------- | ------- | +| `id` | `u32` | Unique group ID | ✅ | +| `farmerid` | `u32` | Farmer/user ID | ✅ | +| `secret` | `string` | Encrypted secret for booting nodes | ❌ | +| `description` | `string` | Group description | ❌ | +| `slapolicy` | `SLAPolicy` | SLA policy details | ❌ | +| `pricingpolicy` | `PricingPolicy` | Pricing policy details | ❌ | +| `compute_slice_normalized_pricing_cc` | `f64` | Pricing per 2GB compute slice in cloud credits | ❌ | +| `storage_slice_normalized_pricing_cc` | `f64` | Pricing per 1GB storage slice in cloud credits | ❌ | +| `reputation` | `int` | Reputation (0-100) | ✅ | +| `uptime` | `int` | Uptime (0-100) | ✅ | + +--- + +## ComputeSlice + +Represents a compute slice (e.g., 1GB memory unit). + +| Field | Type | Description | +| -------------------------- | --------------- | -------------------------------- | +| `nodeid` | `u32` | Owning node ID | +| `id` | `int` | Slice ID in node | +| `mem_gb` | `f64` | Memory in GB | +| `storage_gb` | `f64` | Storage in GB | +| `passmark` | `int` | Passmark score | +| `vcores` | `int` | Virtual cores | +| `cpu_oversubscription` | `int` | CPU oversubscription ratio | +| `storage_oversubscription` | `int` | Storage oversubscription ratio | +| `price_range` | `[]f64` | Price range [min, max] | +| `gpus` | `u8` | Number of GPUs | +| `price_cc` | `f64` | Price per slice in cloud credits | +| `pricing_policy` | `PricingPolicy` | Pricing policy | +| `sla_policy` | `SLAPolicy` | SLA policy | + +--- + +## StorageSlice + +Represents a 1GB storage slice. + +| Field | Type | Description | +| ---------------- | --------------- | -------------------------------- | +| `nodeid` | `u32` | Owning node ID | +| `id` | `int` | Slice ID in node | +| `price_cc` | `f64` | Price per slice in cloud credits | +| `pricing_policy` | `PricingPolicy` | Pricing policy | +| `sla_policy` | `SLAPolicy` | SLA policy | + +--- + +## DeviceInfo + +Hardware device information for a node. + +| Field | Type | Description | +| --------- | ----------------- | ----------------------- | +| `vendor` | `string` | Vendor of the node | +| `storage` | `[]StorageDevice` | List of storage devices | +| `memory` | `[]MemoryDevice` | List of memory devices | +| `cpu` | `[]CPUDevice` | List of CPU devices | +| `gpu` | `[]GPUDevice` | List of GPU devices | +| `network` | `[]NetworkDevice` | List of network devices | + +--- + +## StorageDevice + +| Field | Type | Description | +| ------------- | -------- | --------------------- | +| `id` | `string` | Unique ID for device | +| `size_gb` | `f64` | Size in GB | +| `description` | `string` | Description of device | + +--- + +## MemoryDevice + +| Field | Type | Description | +| ------------- | -------- | --------------------- | +| `id` | `string` | Unique ID for device | +| `size_gb` | `f64` | Size in GB | +| `description` | `string` | Description of device | + +--- + +## CPUDevice + +| Field | Type | Description | +| ------------- | -------- | ------------------------ | +| `id` | `string` | Unique ID for device | +| `cores` | `int` | Number of CPU cores | +| `passmark` | `int` | Passmark benchmark score | +| `description` | `string` | Description of device | +| `cpu_brand` | `string` | Brand of the CPU | +| `cpu_version` | `string` | Version of the CPU | + +--- + +## GPUDevice + +| Field | Type | Description | +| ------------- | -------- | --------------------- | +| `id` | `string` | Unique ID for device | +| `cores` | `int` | Number of GPU cores | +| `memory_gb` | `f64` | GPU memory in GB | +| `description` | `string` | Description of device | +| `gpu_brand` | `string` | Brand of the GPU | +| `gpu_version` | `string` | Version of the GPU | + +--- + +## NetworkDevice + +| Field | Type | Description | +| ------------- | -------- | --------------------- | +| `id` | `string` | Unique ID for device | +| `speed_mbps` | `int` | Network speed in Mbps | +| `description` | `string` | Description of device | + +--- + +## NodeCapacity + +Aggregated hardware capacity for a node. + +| Field | Type | Description | +| ------------ | ----- | ---------------------- | +| `storage_gb` | `f64` | Total storage in GB | +| `mem_gb` | `f64` | Total memory in GB | +| `mem_gb_gpu` | `f64` | Total GPU memory in GB | +| `passmark` | `int` | Total passmark score | +| `vcores` | `int` | Total virtual cores | + +--- + +## SLAPolicy + +Service Level Agreement policy for slices or node groups. + +| Field | Type | Description | +| -------------------- | ----- | --------------------------------------- | +| `sla_uptime` | `int` | Required uptime % (e.g., 90) | +| `sla_bandwidth_mbit` | `int` | Guaranteed bandwidth in Mbps (0 = none) | +| `sla_penalty` | `int` | Penalty % if SLA is breached (0-100) | + +--- + +## PricingPolicy + +Pricing policy for slices or node groups. + +| Field | Type | Description | +| ---------------------------- | ------- | --------------------------------------------------------- | +| `marketplace_year_discounts` | `[]int` | Discounts for 1Y, 2Y, 3Y prepaid usage (e.g. [30,40,50]) | +| `volume_discounts` | `[]int` | Volume discounts based on purchase size (e.g. [10,20,30]) | + + diff --git a/src/objects/grid4/specs/model_bid.v b/src/objects/grid4/specs/model_bid.v new file mode 100644 index 0000000..0ca7b3f --- /dev/null +++ b/src/objects/grid4/specs/model_bid.v @@ -0,0 +1,37 @@ +module datamodel + +// I can bid for infra, and optionally get accepted +@[heap] +pub struct Bid { +pub mut: + id u32 + customer_id u32 // links back to customer for this capacity (user on ledger) + compute_slices_nr int // nr of slices I need in 1 machine + compute_slice_price f64 // price per 1 GB slice I want to accept + storage_slices_nr int + storage_slice_price f64 // price per 1 GB storage slice I want to accept + storage_slices_nr int + status BidStatus + obligation bool // if obligation then will be charged and money needs to be in escrow, otherwise its an intent + start_date u32 // epoch + end_date u32 + signature_user string // signature as done by a user/consumer to validate their identity and intent + billing_period BillingPeriod +} + +pub enum BidStatus { + pending + confirmed + assigned + cancelled + done +} + + +pub enum BillingPeriod { + hourly + monthly + yearly + biannually + triannually +} diff --git a/src/objects/grid4/specs/model_contract.v b/src/objects/grid4/specs/model_contract.v new file mode 100644 index 0000000..f9fc26b --- /dev/null +++ b/src/objects/grid4/specs/model_contract.v @@ -0,0 +1,52 @@ +module datamodel + +// I can bid for infra, and optionally get accepted +@[heap] +pub struct Contract { +pub mut: + id u32 + customer_id u32 // links back to customer for this capacity (user on ledger) + compute_slices []ComputeSliceProvisioned + storage_slices []StorageSliceProvisioned + compute_slice_price f64 // price per 1 GB agreed upon + storage_slice_price f64 // price per 1 GB agreed upon + network_slice_price f64 // price per 1 GB agreed upon (transfer) + status ContractStatus + start_date u32 // epoch + end_date u32 + signature_user string // signature as done by a user/consumer to validate their identity and intent + signature_hoster string // signature as done by the hoster + billing_period BillingPeriod +} + +pub enum ConctractStatus { + active + cancelled + error + paused +} + + +// typically 1GB of memory, but can be adjusted based based on size of machine +pub struct ComputeSliceProvisioned { +pub mut: + node_id u32 + id u16 // the id of the slice in the node + mem_gb f64 + storage_gb f64 + passmark int + vcores int + cpu_oversubscription int + tags string +} + +// 1GB of storage +pub struct StorageSliceProvisioned { +pub mut: + node_id u32 + id u16 // the id of the slice in the node, are tracked in the node itself + storage_size_gb int + tags string +} + + diff --git a/src/objects/grid4/specs/model_node.v b/src/objects/grid4/specs/model_node.v new file mode 100644 index 0000000..b451fa6 --- /dev/null +++ b/src/objects/grid4/specs/model_node.v @@ -0,0 +1,104 @@ +module datamodel + +//ACCESS ONLY TF + +@[heap] +pub struct Node { +pub mut: + id int + nodegroupid int + uptime int // 0..100 + computeslices []ComputeSlice + storageslices []StorageSlice + devices DeviceInfo + country string // 2 letter code as specified in lib/data/countries/data/countryInfo.txt, use that library for validation + capacity NodeCapacity // Hardware capacity details + birthtime u32 // first time node was active + pubkey string + signature_node string // signature done on node to validate pubkey with privkey + signature_farmer string // signature as done by farmers to validate their identity +} + +pub struct DeviceInfo { +pub mut: + vendor string + storage []StorageDevice + memory []MemoryDevice + cpu []CPUDevice + gpu []GPUDevice + network []NetworkDevice +} + +pub struct StorageDevice { +pub mut: + id string // can be used in node + size_gb f64 // Size of the storage device in gigabytes + description string // Description of the storage device +} + +pub struct MemoryDevice { +pub mut: + id string // can be used in node + size_gb f64 // Size of the memory device in gigabytes + description string // Description of the memory device +} + +pub struct CPUDevice { +pub mut: + id string // can be used in node + cores int // Number of CPU cores + passmark int + description string // Description of the CPU + cpu_brand string // Brand of the CPU + cpu_version string // Version of the CPU +} + +pub struct GPUDevice { +pub mut: + id string // can be used in node + cores int // Number of GPU cores + memory_gb f64 // Size of the GPU memory in gigabytes + description string // Description of the GPU + gpu_brand string + gpu_version string +} + +pub struct NetworkDevice { +pub mut: + id string // can be used in node + speed_mbps int // Network speed in Mbps + description string // Description of the network device +} + +// NodeCapacity represents the hardware capacity details of a node. +pub struct NodeCapacity { +pub mut: + storage_gb f64 // Total storage in gigabytes + mem_gb f64 // Total memory in gigabytes + mem_gb_gpu f64 // Total GPU memory in gigabytes + passmark int // Passmark score for the node + vcores int // Total virtual cores +} + +// typically 1GB of memory, but can be adjusted based based on size of machine +pub struct ComputeSlice { +pub mut: + u16 int // the id of the slice in the node + mem_gb f64 + storage_gb f64 + passmark int + vcores int + cpu_oversubscription int + storage_oversubscription int + gpus u8 // nr of GPU's see node to know what GPU's are +} + +// 1GB of storage +pub struct StorageSlice { +pub mut: + u16 int // the id of the slice in the node, are tracked in the node itself +} + +fn (mut n Node) check() ! { + // todo calculate NodeCapacity out of the devices on the Node +} diff --git a/src/objects/grid4/specs/model_nodegroup.v b/src/objects/grid4/specs/model_nodegroup.v new file mode 100644 index 0000000..ae4858b --- /dev/null +++ b/src/objects/grid4/specs/model_nodegroup.v @@ -0,0 +1,33 @@ +module datamodel + +// is a root object, is the only obj farmer needs to configure in the UI, this defines how slices will be created +@[heap] +pub struct NodeGroup { +pub mut: + id u32 + farmerid u32 // link back to farmer who owns the nodegroup, is a user? + secret string // only visible by farmer, in future encrypted, used to boot a node + description string + slapolicy SLAPolicy + pricingpolicy PricingPolicy + compute_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 2GB node slice + storage_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 1GB storage slice + signature_farmer string // signature as done by farmers to validate that they created this group +} + +pub struct SLAPolicy { +pub mut: + sla_uptime int // should +90 + sla_bandwidth_mbit int // minimal mbits we can expect avg over 1h per node, 0 means we don't guarantee + sla_penalty int // 0-100, percent of money given back in relation to month if sla breached, e.g. 200 means we return 2 months worth of rev if sla missed +} + +pub struct PricingPolicy { +pub mut: + marketplace_year_discounts []int = [30, 40, 50] // e.g. 30,40,50 means if user has more CC in wallet than 1 year utilization on all his purchaes then this provider gives 30%, 2Y 40%, ... + // volume_discounts []int = [10, 20, 30] // e.g. 10,20,30 +} + + + + diff --git a/src/objects/grid4/specs/model_reputation.v b/src/objects/grid4/specs/model_reputation.v new file mode 100644 index 0000000..0d65749 --- /dev/null +++ b/src/objects/grid4/specs/model_reputation.v @@ -0,0 +1,19 @@ + +@[heap] +pub struct NodeGroupReputation { +pub mut: + nodegroup_id u32 + reputation int = 50 // between 0 and 100, earned over time + uptime int // between 0 and 100, set by system, farmer has no ability to set this + nodes []NodeReputation +} + +pub struct NodeReputation { +pub mut: + node_id u32 + reputation int = 50 // between 0 and 100, earned over time + uptime int // between 0 and 100, set by system, farmer has no ability to set this +} + + + diff --git a/src/objects/heroledger/dnsrecord.rs b/src/objects/heroledger/dnsrecord.rs new file mode 100644 index 0000000..92fb122 --- /dev/null +++ b/src/objects/heroledger/dnsrecord.rs @@ -0,0 +1,311 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Defines the supported DNS record types +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum NameType { + A, + AAAA, + CNAME, + MX, + TXT, + SRV, + PTR, + NS, +} + +impl Default for NameType { + fn default() -> Self { + NameType::A + } +} + +/// Category of the DNS record +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum NameCat { + IPv4, + IPv6, + Mycelium, +} + +impl Default for NameCat { + fn default() -> Self { + NameCat::IPv4 + } +} + +/// Status of a DNS zone +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum DNSZoneStatus { + Active, + Suspended, + Archived, +} + +impl Default for DNSZoneStatus { + fn default() -> Self { + DNSZoneStatus::Active + } +} + +/// Represents a DNS record configuration +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct DNSRecord { + pub subdomain: String, + pub record_type: NameType, + pub value: String, + pub priority: u32, + pub ttl: u32, + pub is_active: bool, + pub cat: NameCat, + pub is_wildcard: bool, +} + +impl DNSRecord { + pub fn new() -> Self { + Self { + subdomain: String::new(), + record_type: NameType::default(), + value: String::new(), + priority: 0, + ttl: 3600, + is_active: true, + cat: NameCat::default(), + is_wildcard: false, + } + } + + pub fn subdomain(mut self, subdomain: impl ToString) -> Self { + self.subdomain = subdomain.to_string(); + self + } + + pub fn record_type(mut self, record_type: NameType) -> Self { + self.record_type = record_type; + self + } + + pub fn value(mut self, value: impl ToString) -> Self { + self.value = value.to_string(); + self + } + + pub fn priority(mut self, priority: u32) -> Self { + self.priority = priority; + self + } + + pub fn ttl(mut self, ttl: u32) -> Self { + self.ttl = ttl; + self + } + + pub fn is_active(mut self, is_active: bool) -> Self { + self.is_active = is_active; + self + } + + pub fn cat(mut self, cat: NameCat) -> Self { + self.cat = cat; + self + } + + pub fn is_wildcard(mut self, is_wildcard: bool) -> Self { + self.is_wildcard = is_wildcard; + self + } + + pub fn build(self) -> Self { + self + } +} + +impl std::fmt::Display for DNSRecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{:?}", self.subdomain, self.record_type) + } +} + +/// SOA (Start of Authority) record for a DNS zone +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SOARecord { + pub zone_id: u32, + pub primary_ns: String, + pub admin_email: String, + pub serial: u64, + pub refresh: u32, + pub retry: u32, + pub expire: u32, + pub minimum_ttl: u32, + pub is_active: bool, +} + +impl SOARecord { + pub fn new() -> Self { + Self { + zone_id: 0, + primary_ns: String::new(), + admin_email: String::new(), + serial: 0, + refresh: 3600, + retry: 600, + expire: 604800, + minimum_ttl: 3600, + is_active: true, + } + } + + pub fn zone_id(mut self, zone_id: u32) -> Self { + self.zone_id = zone_id; + self + } + + pub fn primary_ns(mut self, primary_ns: impl ToString) -> Self { + self.primary_ns = primary_ns.to_string(); + self + } + + pub fn admin_email(mut self, admin_email: impl ToString) -> Self { + self.admin_email = admin_email.to_string(); + self + } + + pub fn serial(mut self, serial: u64) -> Self { + self.serial = serial; + self + } + + pub fn refresh(mut self, refresh: u32) -> Self { + self.refresh = refresh; + self + } + + pub fn retry(mut self, retry: u32) -> Self { + self.retry = retry; + self + } + + pub fn expire(mut self, expire: u32) -> Self { + self.expire = expire; + self + } + + pub fn minimum_ttl(mut self, minimum_ttl: u32) -> Self { + self.minimum_ttl = minimum_ttl; + self + } + + pub fn is_active(mut self, is_active: bool) -> Self { + self.is_active = is_active; + self + } + + pub fn build(self) -> Self { + self + } +} + +impl std::fmt::Display for SOARecord { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.primary_ns) + } +} + +/// Represents a DNS zone with its configuration and records +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct DNSZone { + /// Base model data + pub base_data: BaseData, + #[index] + pub domain: String, + #[index(path = "subdomain")] + #[index(path = "record_type")] + pub dnsrecords: Vec, + pub administrators: Vec, + pub status: DNSZoneStatus, + pub metadata: HashMap, + #[index(path = "primary_ns")] + pub soarecord: Vec, +} + +impl DNSZone { + /// Create a new DNS zone instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + domain: String::new(), + dnsrecords: Vec::new(), + administrators: Vec::new(), + status: DNSZoneStatus::default(), + metadata: HashMap::new(), + soarecord: Vec::new(), + } + } + + /// Set the domain name (fluent) + pub fn domain(mut self, domain: impl ToString) -> Self { + self.domain = domain.to_string(); + self + } + + /// Add a DNS record (fluent) + pub fn add_dnsrecord(mut self, record: DNSRecord) -> Self { + self.dnsrecords.push(record); + self + } + + /// Set all DNS records (fluent) + pub fn dnsrecords(mut self, dnsrecords: Vec) -> Self { + self.dnsrecords = dnsrecords; + self + } + + /// Add an administrator (fluent) + pub fn add_administrator(mut self, admin_id: u32) -> Self { + self.administrators.push(admin_id); + self + } + + /// Set all administrators (fluent) + pub fn administrators(mut self, administrators: Vec) -> Self { + self.administrators = administrators; + self + } + + /// Set the zone status (fluent) + pub fn status(mut self, status: DNSZoneStatus) -> Self { + self.status = status; + self + } + + /// Add metadata entry (fluent) + pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { + self.metadata.insert(key.to_string(), value.to_string()); + self + } + + /// Set all metadata (fluent) + pub fn metadata(mut self, metadata: HashMap) -> Self { + self.metadata = metadata; + self + } + + /// Add an SOA record (fluent) + pub fn add_soarecord(mut self, soa: SOARecord) -> Self { + self.soarecord.push(soa); + self + } + + /// Set all SOA records (fluent) + pub fn soarecord(mut self, soarecord: Vec) -> Self { + self.soarecord = soarecord; + self + } + + /// Build the final DNS zone instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/heroledger/group.rs b/src/objects/heroledger/group.rs new file mode 100644 index 0000000..5bb941f --- /dev/null +++ b/src/objects/heroledger/group.rs @@ -0,0 +1,227 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; + +/// Defines the lifecycle of a group +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum GroupStatus { + Active, + Inactive, + Suspended, + Archived, +} + +impl Default for GroupStatus { + fn default() -> Self { + GroupStatus::Active + } +} + +/// Visibility controls who can discover or view the group +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Visibility { + Public, // Anyone can see and request to join + Private, // Only invited users can see the group + Unlisted, // Not visible in search; only accessible by direct link or DNS +} + +impl Default for Visibility { + fn default() -> Self { + Visibility::Public + } +} + +/// GroupConfig holds rules that govern group membership and behavior +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct GroupConfig { + pub max_members: u32, + pub allow_guests: bool, + pub auto_approve: bool, + pub require_invite: bool, +} + +impl GroupConfig { + pub fn new() -> Self { + Self { + max_members: 0, + allow_guests: false, + auto_approve: false, + require_invite: false, + } + } + + pub fn max_members(mut self, max_members: u32) -> Self { + self.max_members = max_members; + self + } + + pub fn allow_guests(mut self, allow_guests: bool) -> Self { + self.allow_guests = allow_guests; + self + } + + pub fn auto_approve(mut self, auto_approve: bool) -> Self { + self.auto_approve = auto_approve; + self + } + + pub fn require_invite(mut self, require_invite: bool) -> Self { + self.require_invite = require_invite; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Represents a collaborative or access-controlled unit within the system +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Group { + /// Base model data + pub base_data: BaseData, + #[index] + pub name: String, + pub description: String, + pub dnsrecords: Vec, + pub administrators: Vec, + pub config: GroupConfig, + pub status: GroupStatus, + pub visibility: Visibility, + pub created: u64, + pub updated: u64, +} + +impl Group { + /// Create a new group instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + name: String::new(), + description: String::new(), + dnsrecords: Vec::new(), + administrators: Vec::new(), + config: GroupConfig::new(), + status: GroupStatus::default(), + visibility: Visibility::default(), + created: 0, + updated: 0, + } + } + + /// Set the group name (fluent) + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + /// Set the group description (fluent) + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); + self + } + + /// Add a DNS record ID (fluent) + pub fn add_dnsrecord(mut self, dnsrecord_id: u32) -> Self { + self.dnsrecords.push(dnsrecord_id); + self + } + + /// Set all DNS record IDs (fluent) + pub fn dnsrecords(mut self, dnsrecords: Vec) -> Self { + self.dnsrecords = dnsrecords; + self + } + + /// Add an administrator user ID (fluent) + pub fn add_administrator(mut self, user_id: u32) -> Self { + self.administrators.push(user_id); + self + } + + /// Set all administrator user IDs (fluent) + pub fn administrators(mut self, administrators: Vec) -> Self { + self.administrators = administrators; + self + } + + /// Set the group configuration (fluent) + pub fn config(mut self, config: GroupConfig) -> Self { + self.config = config; + self + } + + /// Set the group status (fluent) + pub fn status(mut self, status: GroupStatus) -> Self { + self.status = status; + self + } + + /// Set the group visibility (fluent) + pub fn visibility(mut self, visibility: Visibility) -> Self { + self.visibility = visibility; + self + } + + /// Set the created timestamp (fluent) + pub fn created(mut self, created: u64) -> Self { + self.created = created; + self + } + + /// Set the updated timestamp (fluent) + pub fn updated(mut self, updated: u64) -> Self { + self.updated = updated; + self + } + + /// Build the final group instance + pub fn build(self) -> Self { + self + } +} + +/// Represents the membership relationship between users and groups +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct UserGroupMembership { + /// Base model data + pub base_data: BaseData, + #[index] + pub user_id: u32, + pub group_ids: Vec, +} + +impl UserGroupMembership { + /// Create a new user group membership instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + user_id: 0, + group_ids: Vec::new(), + } + } + + /// Set the user ID (fluent) + pub fn user_id(mut self, user_id: u32) -> Self { + self.user_id = user_id; + self + } + + /// Add a group ID (fluent) + pub fn add_group_id(mut self, group_id: u32) -> Self { + self.group_ids.push(group_id); + self + } + + /// Set all group IDs (fluent) + pub fn group_ids(mut self, group_ids: Vec) -> Self { + self.group_ids = group_ids; + self + } + + /// Build the final membership instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/heroledger/membership.rs b/src/objects/heroledger/membership.rs new file mode 100644 index 0000000..8b24f54 --- /dev/null +++ b/src/objects/heroledger/membership.rs @@ -0,0 +1,110 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; + +/// Defines the possible roles a member can have +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum MemberRole { + Owner, + Admin, + Moderator, + Member, + Guest, +} + +impl Default for MemberRole { + fn default() -> Self { + MemberRole::Member + } +} + +/// Represents the current status of membership +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum MemberStatus { + Active, + Pending, + Suspended, + Removed, +} + +impl Default for MemberStatus { + fn default() -> Self { + MemberStatus::Pending + } +} + +/// Represents a member within a circle +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Member { + /// Base model data + pub base_data: BaseData, + #[index] + pub user_id: u32, + pub role: MemberRole, + pub status: MemberStatus, + pub joined_at: u64, + pub invited_by: u32, + pub permissions: Vec, +} + +impl Member { + /// Create a new member instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + user_id: 0, + role: MemberRole::default(), + status: MemberStatus::default(), + joined_at: 0, + invited_by: 0, + permissions: Vec::new(), + } + } + + /// Set the user ID (fluent) + pub fn user_id(mut self, user_id: u32) -> Self { + self.user_id = user_id; + self + } + + /// Set the member role (fluent) + pub fn role(mut self, role: MemberRole) -> Self { + self.role = role; + self + } + + /// Set the member status (fluent) + pub fn status(mut self, status: MemberStatus) -> Self { + self.status = status; + self + } + + /// Set the joined timestamp (fluent) + pub fn joined_at(mut self, joined_at: u64) -> Self { + self.joined_at = joined_at; + self + } + + /// Set who invited this member (fluent) + pub fn invited_by(mut self, invited_by: u32) -> Self { + self.invited_by = invited_by; + self + } + + /// Add a permission (fluent) + pub fn add_permission(mut self, permission: impl ToString) -> Self { + self.permissions.push(permission.to_string()); + self + } + + /// Set all permissions (fluent) + pub fn permissions(mut self, permissions: Vec) -> Self { + self.permissions = permissions; + self + } + + /// Build the final member instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/heroledger/mod.rs b/src/objects/heroledger/mod.rs new file mode 100644 index 0000000..4882238 --- /dev/null +++ b/src/objects/heroledger/mod.rs @@ -0,0 +1,10 @@ +// Export all heroledger model modules +pub mod dnsrecord; +pub mod group; +pub mod membership; +pub mod money; +pub mod rhai; +pub mod secretbox; +pub mod signature; +pub mod user; +pub mod user_kvs; diff --git a/src/objects/heroledger/money.rs b/src/objects/heroledger/money.rs new file mode 100644 index 0000000..55c4499 --- /dev/null +++ b/src/objects/heroledger/money.rs @@ -0,0 +1,498 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Represents the status of an account +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum AccountStatus { + Active, + Inactive, + Suspended, + Archived, +} + +impl Default for AccountStatus { + fn default() -> Self { + AccountStatus::Active + } +} + +/// Represents the type of transaction +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TransactionType { + Transfer, + Clawback, + Freeze, + Unfreeze, + Issue, + Burn, +} + +impl Default for TransactionType { + fn default() -> Self { + TransactionType::Transfer + } +} + +/// Represents a signature for transactions +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Signature { + pub signer_id: u32, + pub signature: String, + pub timestamp: u64, +} + +impl Signature { + pub fn new() -> Self { + Self { + signer_id: 0, + signature: String::new(), + timestamp: 0, + } + } + + pub fn signer_id(mut self, signer_id: u32) -> Self { + self.signer_id = signer_id; + self + } + + pub fn signature(mut self, signature: impl ToString) -> Self { + self.signature = signature.to_string(); + self + } + + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Policy item for account operations +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct AccountPolicyItem { + pub signers: Vec, + pub min_signatures: u32, + pub enabled: bool, + pub threshold: f64, + pub recipient: u32, +} + +impl AccountPolicyItem { + pub fn new() -> Self { + Self { + signers: Vec::new(), + min_signatures: 0, + enabled: false, + threshold: 0.0, + recipient: 0, + } + } + + pub fn add_signer(mut self, signer_id: u32) -> Self { + self.signers.push(signer_id); + self + } + + pub fn signers(mut self, signers: Vec) -> Self { + self.signers = signers; + self + } + + pub fn min_signatures(mut self, min_signatures: u32) -> Self { + self.min_signatures = min_signatures; + self + } + + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + pub fn threshold(mut self, threshold: f64) -> Self { + self.threshold = threshold; + self + } + + pub fn recipient(mut self, recipient: u32) -> Self { + self.recipient = recipient; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Represents an account in the financial system +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Account { + /// Base model data + pub base_data: BaseData, + pub owner_id: u32, + #[index] + pub address: String, + pub balance: f64, + pub currency: String, + pub assetid: u32, + pub last_activity: u64, + pub administrators: Vec, + pub accountpolicy: u32, +} + +impl Account { + /// Create a new account instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + owner_id: 0, + address: String::new(), + balance: 0.0, + currency: String::new(), + assetid: 0, + last_activity: 0, + administrators: Vec::new(), + accountpolicy: 0, + } + } + + /// Set the owner ID (fluent) + pub fn owner_id(mut self, owner_id: u32) -> Self { + self.owner_id = owner_id; + self + } + + /// Set the blockchain address (fluent) + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); + self + } + + /// Set the balance (fluent) + pub fn balance(mut self, balance: f64) -> Self { + self.balance = balance; + self + } + + /// Set the currency (fluent) + pub fn currency(mut self, currency: impl ToString) -> Self { + self.currency = currency.to_string(); + self + } + + /// Set the asset ID (fluent) + pub fn assetid(mut self, assetid: u32) -> Self { + self.assetid = assetid; + self + } + + /// Set the last activity timestamp (fluent) + pub fn last_activity(mut self, last_activity: u64) -> Self { + self.last_activity = last_activity; + self + } + + /// Add an administrator (fluent) + pub fn add_administrator(mut self, admin_id: u32) -> Self { + self.administrators.push(admin_id); + self + } + + /// Set all administrators (fluent) + pub fn administrators(mut self, administrators: Vec) -> Self { + self.administrators = administrators; + self + } + + /// Set the account policy ID (fluent) + pub fn accountpolicy(mut self, accountpolicy: u32) -> Self { + self.accountpolicy = accountpolicy; + self + } + + /// Build the final account instance + pub fn build(self) -> Self { + self + } +} + +/// Represents an asset in the financial system +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Asset { + /// Base model data + pub base_data: BaseData, + #[index] + pub address: String, + pub assetid: u32, + pub asset_type: String, + pub issuer: u32, + pub supply: f64, + pub decimals: u8, + pub is_frozen: bool, + pub metadata: HashMap, + pub administrators: Vec, + pub min_signatures: u32, +} + +impl Asset { + /// Create a new asset instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + address: String::new(), + assetid: 0, + asset_type: String::new(), + issuer: 0, + supply: 0.0, + decimals: 0, + is_frozen: false, + metadata: HashMap::new(), + administrators: Vec::new(), + min_signatures: 0, + } + } + + /// Set the blockchain address (fluent) + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); + self + } + + /// Set the asset ID (fluent) + pub fn assetid(mut self, assetid: u32) -> Self { + self.assetid = assetid; + self + } + + /// Set the asset type (fluent) + pub fn asset_type(mut self, asset_type: impl ToString) -> Self { + self.asset_type = asset_type.to_string(); + self + } + + /// Set the issuer (fluent) + pub fn issuer(mut self, issuer: u32) -> Self { + self.issuer = issuer; + self + } + + /// Set the supply (fluent) + pub fn supply(mut self, supply: f64) -> Self { + self.supply = supply; + self + } + + /// Set the decimals (fluent) + pub fn decimals(mut self, decimals: u8) -> Self { + self.decimals = decimals; + self + } + + /// Set the frozen status (fluent) + pub fn is_frozen(mut self, is_frozen: bool) -> Self { + self.is_frozen = is_frozen; + self + } + + /// Add metadata entry (fluent) + pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { + self.metadata.insert(key.to_string(), value.to_string()); + self + } + + /// Set all metadata (fluent) + pub fn metadata(mut self, metadata: HashMap) -> Self { + self.metadata = metadata; + self + } + + /// Add an administrator (fluent) + pub fn add_administrator(mut self, admin_id: u32) -> Self { + self.administrators.push(admin_id); + self + } + + /// Set all administrators (fluent) + pub fn administrators(mut self, administrators: Vec) -> Self { + self.administrators = administrators; + self + } + + /// Set minimum signatures required (fluent) + pub fn min_signatures(mut self, min_signatures: u32) -> Self { + self.min_signatures = min_signatures; + self + } + + /// Build the final asset instance + pub fn build(self) -> Self { + self + } +} + +/// Represents account policies for various operations +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct AccountPolicy { + /// Base model data + pub base_data: BaseData, + pub transferpolicy: AccountPolicyItem, + pub adminpolicy: AccountPolicyItem, + pub clawbackpolicy: AccountPolicyItem, + pub freezepolicy: AccountPolicyItem, +} + +impl AccountPolicy { + /// Create a new account policy instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + transferpolicy: AccountPolicyItem::new(), + adminpolicy: AccountPolicyItem::new(), + clawbackpolicy: AccountPolicyItem::new(), + freezepolicy: AccountPolicyItem::new(), + } + } + + /// Set the transfer policy (fluent) + pub fn transferpolicy(mut self, transferpolicy: AccountPolicyItem) -> Self { + self.transferpolicy = transferpolicy; + self + } + + /// Set the admin policy (fluent) + pub fn adminpolicy(mut self, adminpolicy: AccountPolicyItem) -> Self { + self.adminpolicy = adminpolicy; + self + } + + /// Set the clawback policy (fluent) + pub fn clawbackpolicy(mut self, clawbackpolicy: AccountPolicyItem) -> Self { + self.clawbackpolicy = clawbackpolicy; + self + } + + /// Set the freeze policy (fluent) + pub fn freezepolicy(mut self, freezepolicy: AccountPolicyItem) -> Self { + self.freezepolicy = freezepolicy; + self + } + + /// Build the final account policy instance + pub fn build(self) -> Self { + self + } +} + +/// Represents a financial transaction +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Transaction { + /// Base model data + pub base_data: BaseData, + pub txid: u32, + pub source: u32, + pub destination: u32, + pub assetid: u32, + pub amount: f64, + pub timestamp: u64, + pub status: String, + pub memo: String, + pub tx_type: TransactionType, + pub signatures: Vec, +} + +impl Transaction { + /// Create a new transaction instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + txid: 0, + source: 0, + destination: 0, + assetid: 0, + amount: 0.0, + timestamp: 0, + status: String::new(), + memo: String::new(), + tx_type: TransactionType::default(), + signatures: Vec::new(), + } + } + + /// Set the transaction ID (fluent) + pub fn txid(mut self, txid: u32) -> Self { + self.txid = txid; + self + } + + /// Set the source account (fluent) + pub fn source(mut self, source: u32) -> Self { + self.source = source; + self + } + + /// Set the destination account (fluent) + pub fn destination(mut self, destination: u32) -> Self { + self.destination = destination; + self + } + + /// Set the asset ID (fluent) + pub fn assetid(mut self, assetid: u32) -> Self { + self.assetid = assetid; + self + } + + /// Set the amount (fluent) + pub fn amount(mut self, amount: f64) -> Self { + self.amount = amount; + self + } + + /// Set the timestamp (fluent) + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + /// Set the status (fluent) + pub fn status(mut self, status: impl ToString) -> Self { + self.status = status.to_string(); + self + } + + /// Set the memo (fluent) + pub fn memo(mut self, memo: impl ToString) -> Self { + self.memo = memo.to_string(); + self + } + + /// Set the transaction type (fluent) + pub fn tx_type(mut self, tx_type: TransactionType) -> Self { + self.tx_type = tx_type; + self + } + + /// Add a signature (fluent) + pub fn add_signature(mut self, signature: Signature) -> Self { + self.signatures.push(signature); + self + } + + /// Set all signatures (fluent) + pub fn signatures(mut self, signatures: Vec) -> Self { + self.signatures = signatures; + self + } + + /// Build the final transaction instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/heroledger/rhai.rs b/src/objects/heroledger/rhai.rs new file mode 100644 index 0000000..15aecc6 --- /dev/null +++ b/src/objects/heroledger/rhai.rs @@ -0,0 +1,364 @@ +use ::rhai::plugin::*; +use ::rhai::{Dynamic, Engine, EvalAltResult, Module, CustomType, TypeBuilder}; +use std::mem; + +use crate::objects::heroledger::{ + dnsrecord::DNSZone, + group::{Group, Visibility}, + money::Account, + user::{User, UserStatus}, +}; + +// ============================================================================ +// User Module +// ============================================================================ + +type RhaiUser = User; + +#[export_module] +mod rhai_user_module { + use crate::objects::heroledger::user::User; + + use super::RhaiUser; + + #[rhai_fn(name = "new_user", return_raw)] + pub fn new_user() -> Result> { + Ok(User::new(0)) + } + + #[rhai_fn(name = "username", return_raw)] + pub fn set_username( + user: &mut RhaiUser, + username: String, + ) -> Result> { + let owned = std::mem::take(user); + *user = owned.username(username); + Ok(user.clone()) + } + + #[rhai_fn(name = "add_email", return_raw)] + pub fn add_email(user: &mut RhaiUser, email: String) -> Result> { + let owned = std::mem::take(user); + *user = owned.add_email(email); + Ok(user.clone()) + } + + #[rhai_fn(name = "pubkey", return_raw)] + pub fn set_pubkey(user: &mut RhaiUser, pubkey: String) -> Result> { + let owned = std::mem::take(user); + *user = owned.pubkey(pubkey); + Ok(user.clone()) + } + + #[rhai_fn(name = "status", return_raw)] + pub fn set_status(user: &mut RhaiUser, status: String) -> Result> { + let status_enum = match status.as_str() { + "Active" => UserStatus::Active, + "Inactive" => UserStatus::Inactive, + "Suspended" => UserStatus::Suspended, + "Archived" => UserStatus::Archived, + _ => return Err(format!("Invalid user status: {}", status).into()), + }; + let owned = std::mem::take(user); + *user = owned.status(status_enum); + Ok(user.clone()) + } + + #[rhai_fn(name = "save_user", return_raw)] + pub fn save_user(user: &mut RhaiUser) -> Result> { + // This would integrate with the database save functionality + // For now, just return the user as-is + Ok(user.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(user: &mut RhaiUser) -> u32 { + user.base_data.id + } + + #[rhai_fn(name = "get_username")] + pub fn get_username(user: &mut RhaiUser) -> String { + user.username.clone() + } + + #[rhai_fn(name = "get_email")] + pub fn get_email(user: &mut RhaiUser) -> String { + if let Some(first_email) = user.email.first() { + first_email.clone() + } else { + String::new() + } + } + + #[rhai_fn(name = "get_pubkey")] + pub fn get_pubkey(user: &mut RhaiUser) -> String { + user.pubkey.clone() + } +} + +// ============================================================================ +// Group Module +// ============================================================================ + +type RhaiGroup = Group; + +#[export_module] +mod rhai_group_module { + use super::RhaiGroup; + + #[rhai_fn(name = "new_group", return_raw)] + pub fn new_group() -> Result> { + Ok(Group::new(0)) + } + + #[rhai_fn(name = "name", return_raw)] + pub fn set_name(group: &mut RhaiGroup, name: String) -> Result> { + let owned = std::mem::take(group); + *group = owned.name(name); + Ok(group.clone()) + } + + #[rhai_fn(name = "description", return_raw)] + pub fn set_description( + group: &mut RhaiGroup, + description: String, + ) -> Result> { + let owned = std::mem::take(group); + *group = owned.description(description); + Ok(group.clone()) + } + + #[rhai_fn(name = "visibility", return_raw)] + pub fn set_visibility( + group: &mut RhaiGroup, + visibility: String, + ) -> Result> { + let visibility_enum = match visibility.as_str() { + "Public" => Visibility::Public, + "Private" => Visibility::Private, + _ => return Err(format!("Invalid visibility: {}", visibility).into()), + }; + let owned = std::mem::take(group); + *group = owned.visibility(visibility_enum); + Ok(group.clone()) + } + + #[rhai_fn(name = "save_group", return_raw)] + pub fn save_group(group: &mut RhaiGroup) -> Result> { + Ok(group.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(group: &mut RhaiGroup) -> u32 { + group.base_data.id + } + + #[rhai_fn(name = "get_name")] + pub fn get_name(group: &mut RhaiGroup) -> String { + group.name.clone() + } + + #[rhai_fn(name = "get_description")] + pub fn get_description(group: &mut RhaiGroup) -> String { + group.description.clone() + } +} + +// ============================================================================ +// Account Module (from money.rs) +// ============================================================================ + +type RhaiAccount = Account; + +#[export_module] +mod rhai_account_module { + use super::RhaiAccount; + + #[rhai_fn(name = "new_account", return_raw)] + pub fn new_account() -> Result> { + Ok(Account::new(0)) + } + + #[rhai_fn(name = "owner_id", return_raw)] + pub fn set_owner_id( + account: &mut RhaiAccount, + owner_id: i64, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.owner_id(owner_id as u32); + Ok(account.clone()) + } + + #[rhai_fn(name = "address", return_raw)] + pub fn set_address( + account: &mut RhaiAccount, + address: String, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.address(address); + Ok(account.clone()) + } + + #[rhai_fn(name = "currency", return_raw)] + pub fn set_currency( + account: &mut RhaiAccount, + currency: String, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.currency(currency); + Ok(account.clone()) + } + + #[rhai_fn(name = "save_account", return_raw)] + pub fn save_account(account: &mut RhaiAccount) -> Result> { + Ok(account.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(account: &mut RhaiAccount) -> u32 { + account.base_data.id + } + + #[rhai_fn(name = "get_address")] + pub fn get_address(account: &mut RhaiAccount) -> String { + account.address.clone() + } + + #[rhai_fn(name = "get_currency")] + pub fn get_currency(account: &mut RhaiAccount) -> String { + account.currency.clone() + } +} + +// ============================================================================ +// DNS Zone Module +// ============================================================================ + +type RhaiDNSZone = DNSZone; + +#[export_module] +mod rhai_dns_zone_module { + use super::RhaiDNSZone; + + #[rhai_fn(name = "new_dns_zone", return_raw)] + pub fn new_dns_zone() -> Result> { + Ok(DNSZone::new(0)) + } + + #[rhai_fn(name = "domain", return_raw)] + pub fn set_domain( + zone: &mut RhaiDNSZone, + domain: String, + ) -> Result> { + let owned = std::mem::take(zone); + *zone = owned.domain(domain); + Ok(zone.clone()) + } + + #[rhai_fn(name = "save_dns_zone", return_raw)] + pub fn save_dns_zone(zone: &mut RhaiDNSZone) -> Result> { + Ok(zone.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(zone: &mut RhaiDNSZone) -> u32 { + zone.base_data.id + } + + #[rhai_fn(name = "get_domain")] + pub fn get_domain(zone: &mut RhaiDNSZone) -> String { + zone.domain.clone() + } +} + +// ============================================================================ +// Registration Functions +// ============================================================================ +// Registration functions + +/// Register heroledger modules into a Rhai Module (for use in packages) +/// This flattens all functions into the parent module +pub fn register_heroledger_modules(parent_module: &mut Module) { + // Register custom types + parent_module.set_custom_type::("User"); + parent_module.set_custom_type::("Group"); + parent_module.set_custom_type::("Account"); + parent_module.set_custom_type::("DNSZone"); + + // Merge user functions into parent module + let user_module = exported_module!(rhai_user_module); + parent_module.merge(&user_module); + + // Merge group functions into parent module + let group_module = exported_module!(rhai_group_module); + parent_module.merge(&group_module); + + // Merge account functions into parent module + let account_module = exported_module!(rhai_account_module); + parent_module.merge(&account_module); + + // Merge dnszone functions into parent module + let dnszone_module = exported_module!(rhai_dns_zone_module); + parent_module.merge(&dnszone_module); +} + +/// Register heroledger modules into a Rhai Engine (for standalone use) +pub fn register_user_functions(engine: &mut Engine) { + let module = exported_module!(rhai_user_module); + engine.register_static_module("user", module.into()); +} + +pub fn register_group_functions(engine: &mut Engine) { + let module = exported_module!(rhai_group_module); + engine.register_static_module("group", module.into()); +} + +pub fn register_account_functions(engine: &mut Engine) { + let module = exported_module!(rhai_account_module); + engine.register_static_module("account", module.into()); +} + +pub fn register_dnszone_functions(engine: &mut Engine) { + let module = exported_module!(rhai_dns_zone_module); + engine.register_static_module("dnszone", module.into()); +} + +/// Register all heroledger Rhai modules with the engine +pub fn register_heroledger_rhai_modules(engine: &mut Engine) { + register_user_functions(engine); + register_group_functions(engine); + register_account_functions(engine); + register_dnszone_functions(engine); +} + +// ============================================================================ +// CustomType Implementations (for type registration in Rhai) +// ============================================================================ + +impl CustomType for User { + fn build(mut builder: TypeBuilder) { + builder.with_name("User"); + } +} + +impl CustomType for Group { + fn build(mut builder: TypeBuilder) { + builder.with_name("Group"); + } +} + +impl CustomType for Account { + fn build(mut builder: TypeBuilder) { + builder.with_name("Account"); + } +} + +impl CustomType for DNSZone { + fn build(mut builder: TypeBuilder) { + builder.with_name("DNSZone"); + } +} diff --git a/src/objects/heroledger/secretbox.rs b/src/objects/heroledger/secretbox.rs new file mode 100644 index 0000000..19dd1e7 --- /dev/null +++ b/src/objects/heroledger/secretbox.rs @@ -0,0 +1,137 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; + +/// Category of the secret box +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SecretBoxCategory { + Profile, +} + +impl Default for SecretBoxCategory { + fn default() -> Self { + SecretBoxCategory::Profile + } +} + +/// Status of a notary +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum NotaryStatus { + Active, + Inactive, + Suspended, + Archived, + Error, +} + +impl Default for NotaryStatus { + fn default() -> Self { + NotaryStatus::Active + } +} + +/// Represents an encrypted secret box for storing sensitive data +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SecretBox { + pub notary_id: u32, + pub value: String, + pub version: u16, + pub timestamp: u64, + pub cat: SecretBoxCategory, +} + +impl SecretBox { + pub fn new() -> Self { + Self { + notary_id: 0, + value: String::new(), + version: 1, + timestamp: 0, + cat: SecretBoxCategory::default(), + } + } + + pub fn notary_id(mut self, notary_id: u32) -> Self { + self.notary_id = notary_id; + self + } + + pub fn value(mut self, value: impl ToString) -> Self { + self.value = value.to_string(); + self + } + + pub fn version(mut self, version: u16) -> Self { + self.version = version; + self + } + + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + pub fn cat(mut self, cat: SecretBoxCategory) -> Self { + self.cat = cat; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Represents a notary who can decrypt secret boxes +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Notary { + /// Base model data + pub base_data: BaseData, + #[index] + pub userid: u32, + pub status: NotaryStatus, + pub myceliumaddress: String, + #[index] + pub pubkey: String, +} + +impl Notary { + /// Create a new notary instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + userid: 0, + status: NotaryStatus::default(), + myceliumaddress: String::new(), + pubkey: String::new(), + } + } + + /// Set the user ID (fluent) + pub fn userid(mut self, userid: u32) -> Self { + self.userid = userid; + self + } + + /// Set the notary status (fluent) + pub fn status(mut self, status: NotaryStatus) -> Self { + self.status = status; + self + } + + /// Set the mycelium address (fluent) + pub fn myceliumaddress(mut self, myceliumaddress: impl ToString) -> Self { + self.myceliumaddress = myceliumaddress.to_string(); + self + } + + /// Set the public key (fluent) + pub fn pubkey(mut self, pubkey: impl ToString) -> Self { + self.pubkey = pubkey.to_string(); + self + } + + /// Build the final notary instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/heroledger/signature.rs b/src/objects/heroledger/signature.rs new file mode 100644 index 0000000..5411e94 --- /dev/null +++ b/src/objects/heroledger/signature.rs @@ -0,0 +1,115 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; + +/// Status of a signature +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SignatureStatus { + Active, + Inactive, + Pending, + Revoked, +} + +impl Default for SignatureStatus { + fn default() -> Self { + SignatureStatus::Pending + } +} + +/// Type of object being signed +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ObjectType { + Account, + DNSRecord, + Membership, + User, + Transaction, + KYC, +} + +impl Default for ObjectType { + fn default() -> Self { + ObjectType::User + } +} + +/// Represents a cryptographic signature for various objects +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Signature { + /// Base model data + pub base_data: BaseData, + #[index] + pub signature_id: u32, + #[index] + pub user_id: u32, + pub value: String, + #[index] + pub objectid: u32, + pub objecttype: ObjectType, + pub status: SignatureStatus, + pub timestamp: u64, +} + +impl Signature { + /// Create a new signature instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + signature_id: 0, + user_id: 0, + value: String::new(), + objectid: 0, + objecttype: ObjectType::default(), + status: SignatureStatus::default(), + timestamp: 0, + } + } + + /// Set the signature ID (fluent) + pub fn signature_id(mut self, signature_id: u32) -> Self { + self.signature_id = signature_id; + self + } + + /// Set the user ID (fluent) + pub fn user_id(mut self, user_id: u32) -> Self { + self.user_id = user_id; + self + } + + /// Set the signature value (fluent) + pub fn value(mut self, value: impl ToString) -> Self { + self.value = value.to_string(); + self + } + + /// Set the object ID (fluent) + pub fn objectid(mut self, objectid: u32) -> Self { + self.objectid = objectid; + self + } + + /// Set the object type (fluent) + pub fn objecttype(mut self, objecttype: ObjectType) -> Self { + self.objecttype = objecttype; + self + } + + /// Set the signature status (fluent) + pub fn status(mut self, status: SignatureStatus) -> Self { + self.status = status; + self + } + + /// Set the timestamp (fluent) + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + /// Build the final signature instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/heroledger/user.rs b/src/objects/heroledger/user.rs new file mode 100644 index 0000000..4bb06ac --- /dev/null +++ b/src/objects/heroledger/user.rs @@ -0,0 +1,365 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Represents the status of a user in the system +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum UserStatus { + Active, + Inactive, + Suspended, + Archived, +} + +impl Default for UserStatus { + fn default() -> Self { + UserStatus::Active + } +} + +/// Represents the KYC status of a user +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum KYCStatus { + Pending, + Approved, + Rejected, +} + +impl Default for KYCStatus { + fn default() -> Self { + KYCStatus::Pending + } +} + +/// User profile information +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct UserProfile { + pub user_id: u32, + pub full_name: String, + pub bio: String, + pub profile_pic: String, + pub links: HashMap, + pub metadata: HashMap, +} + +impl UserProfile { + pub fn new() -> Self { + Self { + user_id: 0, + full_name: String::new(), + bio: String::new(), + profile_pic: String::new(), + links: HashMap::new(), + metadata: HashMap::new(), + } + } + + pub fn user_id(mut self, user_id: u32) -> Self { + self.user_id = user_id; + self + } + + pub fn full_name(mut self, full_name: impl ToString) -> Self { + self.full_name = full_name.to_string(); + self + } + + pub fn bio(mut self, bio: impl ToString) -> Self { + self.bio = bio.to_string(); + self + } + + pub fn profile_pic(mut self, profile_pic: impl ToString) -> Self { + self.profile_pic = profile_pic.to_string(); + self + } + + pub fn add_link(mut self, key: impl ToString, value: impl ToString) -> Self { + self.links.insert(key.to_string(), value.to_string()); + self + } + + pub fn links(mut self, links: HashMap) -> Self { + self.links = links; + self + } + + pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { + self.metadata.insert(key.to_string(), value.to_string()); + self + } + + pub fn metadata(mut self, metadata: HashMap) -> Self { + self.metadata = metadata; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// KYC (Know Your Customer) information for a user +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct KYCInfo { + pub user_id: u32, + pub full_name: String, + pub date_of_birth: u64, + pub address: String, + pub phone_number: String, + pub id_number: String, + pub id_type: String, + pub id_expiry: u64, + pub kyc_status: KYCStatus, + pub kyc_verified: bool, + pub kyc_verified_by: u32, + pub kyc_verified_at: u64, + pub kyc_rejected_reason: String, + pub kyc_signature: u32, + pub metadata: HashMap, +} + +impl KYCInfo { + pub fn new() -> Self { + Self { + user_id: 0, + full_name: String::new(), + date_of_birth: 0, + address: String::new(), + phone_number: String::new(), + id_number: String::new(), + id_type: String::new(), + id_expiry: 0, + kyc_status: KYCStatus::default(), + kyc_verified: false, + kyc_verified_by: 0, + kyc_verified_at: 0, + kyc_rejected_reason: String::new(), + kyc_signature: 0, + metadata: HashMap::new(), + } + } + + pub fn user_id(mut self, user_id: u32) -> Self { + self.user_id = user_id; + self + } + + pub fn full_name(mut self, full_name: impl ToString) -> Self { + self.full_name = full_name.to_string(); + self + } + + pub fn date_of_birth(mut self, date_of_birth: u64) -> Self { + self.date_of_birth = date_of_birth; + self + } + + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); + self + } + + pub fn phone_number(mut self, phone_number: impl ToString) -> Self { + self.phone_number = phone_number.to_string(); + self + } + + pub fn id_number(mut self, id_number: impl ToString) -> Self { + self.id_number = id_number.to_string(); + self + } + + pub fn id_type(mut self, id_type: impl ToString) -> Self { + self.id_type = id_type.to_string(); + self + } + + pub fn id_expiry(mut self, id_expiry: u64) -> Self { + self.id_expiry = id_expiry; + self + } + + pub fn kyc_status(mut self, kyc_status: KYCStatus) -> Self { + self.kyc_status = kyc_status; + self + } + + pub fn kyc_verified(mut self, kyc_verified: bool) -> Self { + self.kyc_verified = kyc_verified; + self + } + + pub fn kyc_verified_by(mut self, kyc_verified_by: u32) -> Self { + self.kyc_verified_by = kyc_verified_by; + self + } + + pub fn kyc_verified_at(mut self, kyc_verified_at: u64) -> Self { + self.kyc_verified_at = kyc_verified_at; + self + } + + pub fn kyc_rejected_reason(mut self, kyc_rejected_reason: impl ToString) -> Self { + self.kyc_rejected_reason = kyc_rejected_reason.to_string(); + self + } + + pub fn kyc_signature(mut self, kyc_signature: u32) -> Self { + self.kyc_signature = kyc_signature; + self + } + + pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { + self.metadata.insert(key.to_string(), value.to_string()); + self + } + + pub fn metadata(mut self, metadata: HashMap) -> Self { + self.metadata = metadata; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Represents a secret box for storing encrypted data +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SecretBox { + pub data: Vec, + pub nonce: Vec, +} + +impl SecretBox { + pub fn new() -> Self { + Self { + data: Vec::new(), + nonce: Vec::new(), + } + } + + pub fn data(mut self, data: Vec) -> Self { + self.data = data; + self + } + + pub fn nonce(mut self, nonce: Vec) -> Self { + self.nonce = nonce; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Represents a user in the heroledger system +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)] +pub struct User { + /// Base model data + pub base_data: BaseData, + #[index] + pub username: String, + #[index] + pub pubkey: String, + pub email: Vec, + pub status: UserStatus, + pub userprofile: Vec, + pub kyc: Vec, +} + +impl Default for User { + fn default() -> Self { + Self { + base_data: BaseData::new(), + username: String::new(), + pubkey: String::new(), + email: Vec::new(), + status: UserStatus::default(), + userprofile: Vec::new(), + kyc: Vec::new(), + } + } +} + +impl User { + /// Create a new user instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + username: String::new(), + pubkey: String::new(), + email: Vec::new(), + status: UserStatus::default(), + userprofile: Vec::new(), + kyc: Vec::new(), + } + } + + /// Get the user ID + pub fn id(&self) -> u32 { + self.base_data.id + } + + /// Set the username (fluent) + pub fn username(mut self, username: impl ToString) -> Self { + self.username = username.to_string(); + self + } + + /// Set the public key (fluent) + pub fn pubkey(mut self, pubkey: impl ToString) -> Self { + self.pubkey = pubkey.to_string(); + self + } + + /// Add an email address (fluent) + pub fn add_email(mut self, email: impl ToString) -> Self { + self.email.push(email.to_string()); + self + } + + /// Set all email addresses (fluent) + pub fn email(mut self, email: Vec) -> Self { + self.email = email; + self + } + + /// Set the user status (fluent) + pub fn status(mut self, status: UserStatus) -> Self { + self.status = status; + self + } + + /// Add a user profile secret box (fluent) + pub fn add_userprofile(mut self, profile: SecretBox) -> Self { + self.userprofile.push(profile); + self + } + + /// Set all user profile secret boxes (fluent) + pub fn userprofile(mut self, userprofile: Vec) -> Self { + self.userprofile = userprofile; + self + } + + /// Add a KYC secret box (fluent) + pub fn add_kyc(mut self, kyc: SecretBox) -> Self { + self.kyc.push(kyc); + self + } + + /// Set all KYC secret boxes (fluent) + pub fn kyc(mut self, kyc: Vec) -> Self { + self.kyc = kyc; + self + } + + /// Build the final user instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/heroledger/user_kvs.rs b/src/objects/heroledger/user_kvs.rs new file mode 100644 index 0000000..582e8b7 --- /dev/null +++ b/src/objects/heroledger/user_kvs.rs @@ -0,0 +1,111 @@ +use super::secretbox::SecretBox; +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; + +/// Represents a per-user key-value store +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct UserKVS { + /// Base model data + pub base_data: BaseData, + #[index] + pub userid: u32, + pub name: String, +} + +impl UserKVS { + /// Create a new user KVS instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + userid: 0, + name: String::new(), + } + } + + /// Set the user ID (fluent) + pub fn userid(mut self, userid: u32) -> Self { + self.userid = userid; + self + } + + /// Set the KVS name (fluent) + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + /// Build the final user KVS instance + pub fn build(self) -> Self { + self + } +} + +/// Represents an item in a user's key-value store +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct UserKVSItem { + /// Base model data + pub base_data: BaseData, + #[index] + pub userkvs_id: u32, + pub key: String, + pub value: String, + pub secretbox: Vec, + pub timestamp: u64, +} + +impl UserKVSItem { + /// Create a new user KVS item instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + userkvs_id: 0, + key: String::new(), + value: String::new(), + secretbox: Vec::new(), + timestamp: 0, + } + } + + /// Set the user KVS ID (fluent) + pub fn userkvs_id(mut self, userkvs_id: u32) -> Self { + self.userkvs_id = userkvs_id; + self + } + + /// Set the key (fluent) + pub fn key(mut self, key: impl ToString) -> Self { + self.key = key.to_string(); + self + } + + /// Set the value (fluent) + pub fn value(mut self, value: impl ToString) -> Self { + self.value = value.to_string(); + self + } + + /// Add a secret box (fluent) + pub fn add_secretbox(mut self, secretbox: SecretBox) -> Self { + self.secretbox.push(secretbox); + self + } + + /// Set all secret boxes (fluent) + pub fn secretbox(mut self, secretbox: Vec) -> Self { + self.secretbox = secretbox; + self + } + + /// Set the timestamp (fluent) + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + /// Build the final user KVS item instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/kyc/client.rs b/src/objects/kyc/client.rs new file mode 100644 index 0000000..22bac7f --- /dev/null +++ b/src/objects/kyc/client.rs @@ -0,0 +1,238 @@ +/// KYC Client +/// +/// Actual API client for making KYC provider API calls. +/// Currently implements Idenfy API but designed to be extensible for other providers. + +use serde::{Deserialize, Serialize}; +use super::{KycInfo, KycSession, session::SessionStatus}; + +/// KYC Client for making API calls to KYC providers +#[derive(Debug, Clone)] +pub struct KycClient { + /// Provider name (e.g., "idenfy", "sumsub", "onfido") + pub provider: String, + + /// API key + pub api_key: String, + + /// API secret + pub api_secret: String, + + /// Base URL for API (optional, uses provider default if not set) + pub base_url: Option, +} + +/// Idenfy-specific API request/response structures +#[derive(Debug, Serialize, Deserialize)] +pub struct IdenfyTokenRequest { + #[serde(rename = "clientId")] + pub client_id: String, + + #[serde(rename = "firstName")] + pub first_name: String, + + #[serde(rename = "lastName")] + pub last_name: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub email: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub phone: Option, + + #[serde(rename = "dateOfBirth", skip_serializing_if = "Option::is_none")] + pub date_of_birth: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub nationality: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub address: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option, + + #[serde(rename = "zipCode", skip_serializing_if = "Option::is_none")] + pub zip_code: Option, + + #[serde(rename = "successUrl", skip_serializing_if = "Option::is_none")] + pub success_url: Option, + + #[serde(rename = "errorUrl", skip_serializing_if = "Option::is_none")] + pub error_url: Option, + + #[serde(rename = "callbackUrl", skip_serializing_if = "Option::is_none")] + pub callback_url: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub locale: Option, +} + +#[derive(Debug, Deserialize)] +pub struct IdenfyTokenResponse { + #[serde(rename = "authToken")] + pub auth_token: String, + + #[serde(rename = "scanRef")] + pub scan_ref: String, + + #[serde(rename = "clientId")] + pub client_id: String, +} + +#[derive(Debug, Deserialize)] +pub struct IdenfyVerificationStatus { + pub status: String, + + #[serde(rename = "scanRef")] + pub scan_ref: String, + + #[serde(rename = "clientId")] + pub client_id: String, +} + +impl KycClient { + /// Create a new KYC client + pub fn new(provider: String, api_key: String, api_secret: String) -> Self { + Self { + provider, + api_key, + api_secret, + base_url: None, + } + } + + /// Create an Idenfy client + pub fn idenfy(api_key: String, api_secret: String) -> Self { + Self { + provider: "idenfy".to_string(), + api_key, + api_secret, + base_url: Some("https://ivs.idenfy.com/api/v2".to_string()), + } + } + + /// Set custom base URL + pub fn with_base_url(mut self, base_url: String) -> Self { + self.base_url = Some(base_url); + self + } + + /// Get the base URL for the provider + fn get_base_url(&self) -> String { + if let Some(url) = &self.base_url { + return url.clone(); + } + + match self.provider.as_str() { + "idenfy" => "https://ivs.idenfy.com/api/v2".to_string(), + "sumsub" => "https://api.sumsub.com".to_string(), + "onfido" => "https://api.onfido.com/v3".to_string(), + _ => panic!("Unknown provider: {}", self.provider), + } + } + + /// Create a verification session (Idenfy implementation) + pub async fn create_verification_session( + &self, + kyc_info: &KycInfo, + session: &mut KycSession, + ) -> Result> { + match self.provider.as_str() { + "idenfy" => self.create_idenfy_session(kyc_info, session).await, + _ => Err(format!("Provider {} not yet implemented", self.provider).into()), + } + } + + /// Create an Idenfy verification session + async fn create_idenfy_session( + &self, + kyc_info: &KycInfo, + session: &mut KycSession, + ) -> Result> { + let url = format!("{}/token", self.get_base_url()); + + let request = IdenfyTokenRequest { + client_id: kyc_info.client_id.clone(), + first_name: kyc_info.first_name.clone(), + last_name: kyc_info.last_name.clone(), + email: kyc_info.email.clone(), + phone: kyc_info.phone.clone(), + date_of_birth: kyc_info.date_of_birth.clone(), + nationality: kyc_info.nationality.clone(), + address: kyc_info.address.clone(), + city: kyc_info.city.clone(), + country: kyc_info.country.clone(), + zip_code: kyc_info.postal_code.clone(), + success_url: session.success_url.clone(), + error_url: session.error_url.clone(), + callback_url: session.callback_url.clone(), + locale: session.locale.clone(), + }; + + let client = reqwest::Client::new(); + let response = client + .post(&url) + .basic_auth(&self.api_key, Some(&self.api_secret)) + .json(&request) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(format!("Idenfy API error: {}", error_text).into()); + } + + let token_response: IdenfyTokenResponse = response.json().await?; + + // Update session with token and URL + session.set_session_token(token_response.auth_token.clone()); + + // Construct verification URL + let verification_url = format!( + "https://ivs.idenfy.com/api/v2/redirect?authToken={}", + token_response.auth_token + ); + session.set_verification_url(verification_url.clone()); + session.set_status(SessionStatus::Active); + + Ok(verification_url) + } + + /// Get verification status (Idenfy implementation) + pub async fn get_verification_status( + &self, + scan_ref: &str, + ) -> Result> { + match self.provider.as_str() { + "idenfy" => self.get_idenfy_status(scan_ref).await, + _ => Err(format!("Provider {} not yet implemented", self.provider).into()), + } + } + + /// Get Idenfy verification status + async fn get_idenfy_status( + &self, + scan_ref: &str, + ) -> Result> { + let url = format!("{}/status/{}", self.get_base_url(), scan_ref); + + let client = reqwest::Client::new(); + let response = client + .get(&url) + .basic_auth(&self.api_key, Some(&self.api_secret)) + .send() + .await?; + + if !response.status().is_success() { + let error_text = response.text().await?; + return Err(format!("Idenfy API error: {}", error_text).into()); + } + + let status: IdenfyVerificationStatus = response.json().await?; + Ok(status) + } +} diff --git a/src/objects/kyc/info.rs b/src/objects/kyc/info.rs new file mode 100644 index 0000000..f2ece62 --- /dev/null +++ b/src/objects/kyc/info.rs @@ -0,0 +1,319 @@ +/// KYC Info Object +/// +/// Represents customer/person information for KYC verification. +/// Designed to be provider-agnostic but follows Idenfy API patterns. + +use crate::store::{BaseData, Object, Storable}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)] +pub struct KycInfo { + #[serde(flatten)] + pub base_data: BaseData, + + /// External client ID (from your system) - links to User + pub client_id: String, + + /// Full name (or separate first/last) + pub full_name: String, + + /// First name + pub first_name: String, + + /// Last name + pub last_name: String, + + /// Email address + pub email: Option, + + /// Phone number + pub phone: Option, + + /// Date of birth (YYYY-MM-DD string or unix timestamp) + pub date_of_birth: Option, + + /// Date of birth as unix timestamp + pub date_of_birth_timestamp: Option, + + /// Nationality (ISO 3166-1 alpha-2 code) + pub nationality: Option, + + /// Address + pub address: Option, + + /// City + pub city: Option, + + /// Country (ISO 3166-1 alpha-2 code) + pub country: Option, + + /// Postal code + pub postal_code: Option, + + /// ID document number + pub id_number: Option, + + /// ID document type (passport, drivers_license, national_id, etc.) + pub id_type: Option, + + /// ID document expiry (unix timestamp) + pub id_expiry: Option, + + /// KYC provider (e.g., "idenfy", "sumsub", "onfido") + pub provider: String, + + /// Provider-specific client ID (assigned by KYC provider) + pub provider_client_id: Option, + + /// Current verification status + pub verification_status: VerificationStatus, + + /// Whether KYC is verified + pub kyc_verified: bool, + + /// User ID who verified this KYC + pub kyc_verified_by: Option, + + /// Timestamp when KYC was verified + pub kyc_verified_at: Option, + + /// Reason for rejection if denied + pub kyc_rejected_reason: Option, + + /// Signature ID for verification record + pub kyc_signature: Option, + + /// Additional metadata + #[serde(default)] + pub metadata: std::collections::HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "UPPERCASE")] +pub enum VerificationStatus { + /// Not yet started + Pending, + /// Verification in progress + Processing, + /// Successfully verified + Approved, + /// Verification failed + Denied, + /// Verification expired + Expired, + /// Requires manual review + Review, +} + +impl Default for VerificationStatus { + fn default() -> Self { + VerificationStatus::Pending + } +} + +impl KycInfo { + /// Create a new KYC info object + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + base_data.id = id; + Self { + base_data, + client_id: String::new(), + full_name: String::new(), + first_name: String::new(), + last_name: String::new(), + email: None, + phone: None, + date_of_birth: None, + date_of_birth_timestamp: None, + nationality: None, + address: None, + city: None, + country: None, + postal_code: None, + id_number: None, + id_type: None, + id_expiry: None, + provider: "idenfy".to_string(), // Default to Idenfy + provider_client_id: None, + verification_status: VerificationStatus::default(), + kyc_verified: false, + kyc_verified_by: None, + kyc_verified_at: None, + kyc_rejected_reason: None, + kyc_signature: None, + metadata: std::collections::HashMap::new(), + } + } + + /// Builder: Set client ID + pub fn client_id(mut self, client_id: String) -> Self { + self.client_id = client_id; + self.base_data.update_modified(); + self + } + + /// Builder: Set full name + pub fn full_name(mut self, full_name: String) -> Self { + self.full_name = full_name.clone(); + // Try to split into first/last if not already set + if self.first_name.is_empty() && self.last_name.is_empty() { + let parts: Vec<&str> = full_name.split_whitespace().collect(); + if parts.len() >= 2 { + self.first_name = parts[0].to_string(); + self.last_name = parts[1..].join(" "); + } else if parts.len() == 1 { + self.first_name = parts[0].to_string(); + } + } + self.base_data.update_modified(); + self + } + + /// Builder: Set first name + pub fn first_name(mut self, first_name: String) -> Self { + self.first_name = first_name.clone(); + // Update full_name if last_name exists + if !self.last_name.is_empty() { + self.full_name = format!("{} {}", first_name, self.last_name); + } else { + self.full_name = first_name; + } + self.base_data.update_modified(); + self + } + + /// Builder: Set last name + pub fn last_name(mut self, last_name: String) -> Self { + self.last_name = last_name.clone(); + // Update full_name if first_name exists + if !self.first_name.is_empty() { + self.full_name = format!("{} {}", self.first_name, last_name); + } else { + self.full_name = last_name; + } + self.base_data.update_modified(); + self + } + + /// Builder: Set email + pub fn email(mut self, email: String) -> Self { + self.email = Some(email); + self.base_data.update_modified(); + self + } + + /// Builder: Set phone + pub fn phone(mut self, phone: String) -> Self { + self.phone = Some(phone); + self.base_data.update_modified(); + self + } + + /// Builder: Set date of birth + pub fn date_of_birth(mut self, dob: String) -> Self { + self.date_of_birth = Some(dob); + self.base_data.update_modified(); + self + } + + /// Builder: Set nationality + pub fn nationality(mut self, nationality: String) -> Self { + self.nationality = Some(nationality); + self.base_data.update_modified(); + self + } + + /// Builder: Set address + pub fn address(mut self, address: String) -> Self { + self.address = Some(address); + self.base_data.update_modified(); + self + } + + /// Builder: Set city + pub fn city(mut self, city: String) -> Self { + self.city = Some(city); + self.base_data.update_modified(); + self + } + + /// Builder: Set country + pub fn country(mut self, country: String) -> Self { + self.country = Some(country); + self.base_data.update_modified(); + self + } + + /// Builder: Set postal code + pub fn postal_code(mut self, postal_code: String) -> Self { + self.postal_code = Some(postal_code); + self.base_data.update_modified(); + self + } + + /// Builder: Set ID number + pub fn id_number(mut self, id_number: String) -> Self { + self.id_number = Some(id_number); + self.base_data.update_modified(); + self + } + + /// Builder: Set ID type + pub fn id_type(mut self, id_type: String) -> Self { + self.id_type = Some(id_type); + self.base_data.update_modified(); + self + } + + /// Builder: Set ID expiry + pub fn id_expiry(mut self, id_expiry: u64) -> Self { + self.id_expiry = Some(id_expiry); + self.base_data.update_modified(); + self + } + + /// Builder: Set KYC provider + pub fn provider(mut self, provider: String) -> Self { + self.provider = provider; + self.base_data.update_modified(); + self + } + + /// Set provider client ID (assigned by KYC provider) + pub fn set_provider_client_id(&mut self, provider_client_id: String) { + self.provider_client_id = Some(provider_client_id); + self.base_data.update_modified(); + } + + /// Set verification status + pub fn set_verification_status(&mut self, status: VerificationStatus) { + self.verification_status = status; + self.base_data.update_modified(); + } + + /// Set KYC verified + pub fn set_kyc_verified(&mut self, verified: bool, verified_by: Option) { + self.kyc_verified = verified; + self.kyc_verified_by = verified_by; + self.kyc_verified_at = Some(std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs()); + self.base_data.update_modified(); + } + + /// Set KYC rejected + pub fn set_kyc_rejected(&mut self, reason: String) { + self.kyc_verified = false; + self.kyc_rejected_reason = Some(reason); + self.verification_status = VerificationStatus::Denied; + self.base_data.update_modified(); + } + + /// Add metadata + pub fn add_metadata(&mut self, key: String, value: String) { + self.metadata.insert(key, value); + self.base_data.update_modified(); + } +} diff --git a/src/objects/kyc/mod.rs b/src/objects/kyc/mod.rs new file mode 100644 index 0000000..91364b8 --- /dev/null +++ b/src/objects/kyc/mod.rs @@ -0,0 +1,13 @@ +/// KYC (Know Your Customer) Module +/// +/// Provides generic KYC client and session management. +/// Designed to work with multiple KYC providers (Idenfy, Sumsub, Onfido, etc.) + +pub mod info; +pub mod client; +pub mod session; +pub mod rhai; + +pub use info::{KycInfo, VerificationStatus}; +pub use client::KycClient; +pub use session::{KycSession, SessionStatus, SessionResult}; diff --git a/src/objects/kyc/rhai.rs b/src/objects/kyc/rhai.rs new file mode 100644 index 0000000..e522d1a --- /dev/null +++ b/src/objects/kyc/rhai.rs @@ -0,0 +1,326 @@ +/// Rhai bindings for KYC objects + +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; +use std::mem; + +use super::info::{KycInfo, VerificationStatus}; +use super::session::{KycSession, SessionStatus}; + +// ============================================================================ +// KYC Info Module +// ============================================================================ + +type RhaiKycInfo = KycInfo; + +#[export_module] +mod rhai_kyc_info_module { + use super::RhaiKycInfo; + + #[rhai_fn(name = "new_kyc_info", return_raw)] + pub fn new_kyc_info() -> Result> { + Ok(KycInfo::new(0)) + } + + #[rhai_fn(name = "client_id", return_raw)] + pub fn set_client_id( + info: &mut RhaiKycInfo, + client_id: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.client_id(client_id); + Ok(info.clone()) + } + + #[rhai_fn(name = "first_name", return_raw)] + pub fn set_first_name( + info: &mut RhaiKycInfo, + first_name: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.first_name(first_name); + Ok(info.clone()) + } + + #[rhai_fn(name = "last_name", return_raw)] + pub fn set_last_name( + info: &mut RhaiKycInfo, + last_name: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.last_name(last_name); + Ok(info.clone()) + } + + #[rhai_fn(name = "email", return_raw)] + pub fn set_email( + info: &mut RhaiKycInfo, + email: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.email(email); + Ok(info.clone()) + } + + #[rhai_fn(name = "phone", return_raw)] + pub fn set_phone( + info: &mut RhaiKycInfo, + phone: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.phone(phone); + Ok(info.clone()) + } + + #[rhai_fn(name = "date_of_birth", return_raw)] + pub fn set_date_of_birth( + info: &mut RhaiKycInfo, + dob: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.date_of_birth(dob); + Ok(info.clone()) + } + + #[rhai_fn(name = "nationality", return_raw)] + pub fn set_nationality( + info: &mut RhaiKycInfo, + nationality: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.nationality(nationality); + Ok(info.clone()) + } + + #[rhai_fn(name = "address", return_raw)] + pub fn set_address( + info: &mut RhaiKycInfo, + address: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.address(address); + Ok(info.clone()) + } + + #[rhai_fn(name = "city", return_raw)] + pub fn set_city( + info: &mut RhaiKycInfo, + city: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.city(city); + Ok(info.clone()) + } + + #[rhai_fn(name = "country", return_raw)] + pub fn set_country( + info: &mut RhaiKycInfo, + country: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.country(country); + Ok(info.clone()) + } + + #[rhai_fn(name = "postal_code", return_raw)] + pub fn set_postal_code( + info: &mut RhaiKycInfo, + postal_code: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.postal_code(postal_code); + Ok(info.clone()) + } + + #[rhai_fn(name = "provider", return_raw)] + pub fn set_provider( + info: &mut RhaiKycInfo, + provider: String, + ) -> Result> { + let owned = std::mem::take(info); + *info = owned.provider(provider); + Ok(info.clone()) + } + + #[rhai_fn(name = "document_type", return_raw)] + pub fn set_document_type( + info: &mut RhaiKycInfo, + doc_type: String, + ) -> Result> { + // Store in provider field for now (or add to KycInfo struct) + let provider = info.provider.clone(); + let owned = std::mem::take(info); + *info = owned.provider(format!("{}|doc_type:{}", provider, doc_type)); + Ok(info.clone()) + } + + #[rhai_fn(name = "document_number", return_raw)] + pub fn set_document_number( + info: &mut RhaiKycInfo, + doc_number: String, + ) -> Result> { + // Store in provider field for now (or add to KycInfo struct) + let provider = info.provider.clone(); + let owned = std::mem::take(info); + *info = owned.provider(format!("{}|doc_num:{}", provider, doc_number)); + Ok(info.clone()) + } + + #[rhai_fn(name = "verified", return_raw)] + pub fn set_verified( + info: &mut RhaiKycInfo, + _verified: bool, + ) -> Result> { + // Mark as verified in provider field + let provider = info.provider.clone(); + let owned = std::mem::take(info); + *info = owned.provider(format!("{}|verified", provider)); + Ok(info.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(info: &mut RhaiKycInfo) -> u32 { + info.base_data.id + } + + #[rhai_fn(name = "get_client_id")] + pub fn get_client_id(info: &mut RhaiKycInfo) -> String { + info.client_id.clone() + } + + #[rhai_fn(name = "get_first_name")] + pub fn get_first_name(info: &mut RhaiKycInfo) -> String { + info.first_name.clone() + } + + #[rhai_fn(name = "get_last_name")] + pub fn get_last_name(info: &mut RhaiKycInfo) -> String { + info.last_name.clone() + } + + #[rhai_fn(name = "get_email")] + pub fn get_email(info: &mut RhaiKycInfo) -> String { + info.email.clone().unwrap_or_default() + } + + #[rhai_fn(name = "get_provider")] + pub fn get_provider(info: &mut RhaiKycInfo) -> String { + info.provider.clone() + } +} + +// ============================================================================ +// KYC Session Module +// ============================================================================ + +type RhaiKycSession = KycSession; + +#[export_module] +mod rhai_kyc_session_module { + use super::RhaiKycSession; + + #[rhai_fn(name = "new_kyc_session", return_raw)] + pub fn new_kyc_session( + client_id: String, + provider: String, + ) -> Result> { + Ok(KycSession::new(0, client_id, provider)) + } + + #[rhai_fn(name = "callback_url", return_raw)] + pub fn set_callback_url( + session: &mut RhaiKycSession, + url: String, + ) -> Result> { + let owned = std::mem::take(session); + *session = owned.callback_url(url); + Ok(session.clone()) + } + + #[rhai_fn(name = "success_url", return_raw)] + pub fn set_success_url( + session: &mut RhaiKycSession, + url: String, + ) -> Result> { + let owned = std::mem::take(session); + *session = owned.success_url(url); + Ok(session.clone()) + } + + #[rhai_fn(name = "error_url", return_raw)] + pub fn set_error_url( + session: &mut RhaiKycSession, + url: String, + ) -> Result> { + let owned = std::mem::take(session); + *session = owned.error_url(url); + Ok(session.clone()) + } + + #[rhai_fn(name = "locale", return_raw)] + pub fn set_locale( + session: &mut RhaiKycSession, + locale: String, + ) -> Result> { + let owned = std::mem::take(session); + *session = owned.locale(locale); + Ok(session.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(session: &mut RhaiKycSession) -> u32 { + session.base_data.id + } + + #[rhai_fn(name = "get_client_id")] + pub fn get_client_id(session: &mut RhaiKycSession) -> String { + session.client_id.clone() + } + + #[rhai_fn(name = "get_provider")] + pub fn get_provider(session: &mut RhaiKycSession) -> String { + session.provider.clone() + } + + #[rhai_fn(name = "get_verification_url")] + pub fn get_verification_url(session: &mut RhaiKycSession) -> String { + session.verification_url.clone().unwrap_or_default() + } +} + +// ============================================================================ +// Registration Functions +// ============================================================================ + +/// Register KYC modules into a Rhai Module (for use in packages) +pub fn register_kyc_modules(parent_module: &mut Module) { + // Register custom types + parent_module.set_custom_type::("KycInfo"); + parent_module.set_custom_type::("KycSession"); + + // Merge KYC info functions + let info_module = exported_module!(rhai_kyc_info_module); + parent_module.merge(&info_module); + + // Merge KYC session functions + let session_module = exported_module!(rhai_kyc_session_module); + parent_module.merge(&session_module); +} + +// ============================================================================ +// CustomType Implementations +// ============================================================================ + +impl CustomType for KycInfo { + fn build(mut builder: TypeBuilder) { + builder.with_name("KycInfo"); + } +} + +impl CustomType for KycSession { + fn build(mut builder: TypeBuilder) { + builder.with_name("KycSession"); + } +} diff --git a/src/objects/kyc/session.rs b/src/objects/kyc/session.rs new file mode 100644 index 0000000..ac8fd93 --- /dev/null +++ b/src/objects/kyc/session.rs @@ -0,0 +1,186 @@ +/// KYC Verification Session +/// +/// Represents a verification session for a KYC client. +/// Follows Idenfy API patterns but is provider-agnostic. + +use crate::store::{BaseData, Object, Storable}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)] +pub struct KycSession { + #[serde(flatten)] + pub base_data: BaseData, + + /// Reference to the KYC client + pub client_id: String, + + /// KYC provider + pub provider: String, + + /// Session token/ID from provider + pub session_token: Option, + + /// Verification URL for the client + pub verification_url: Option, + + /// Session status + pub status: SessionStatus, + + /// Session expiration timestamp + pub expires_at: Option, + + /// Callback URL for webhook notifications + pub callback_url: Option, + + /// Success redirect URL + pub success_url: Option, + + /// Error redirect URL + pub error_url: Option, + + /// Locale (e.g., "en", "de", "fr") + pub locale: Option, + + /// Provider-specific configuration + #[serde(default)] + pub provider_config: std::collections::HashMap, + + /// Session result data + pub result: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[serde(rename_all = "UPPERCASE")] +pub enum SessionStatus { + /// Session created but not started + #[default] + Created, + /// Client is currently verifying + Active, + /// Session completed successfully + Completed, + /// Session failed + Failed, + /// Session expired + Expired, + /// Session cancelled + Cancelled, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionResult { + /// Overall verification status + pub status: String, + + /// Verification score (0-100) + pub score: Option, + + /// Reason for denial (if denied) + pub denial_reason: Option, + + /// Document type verified + pub document_type: Option, + + /// Document number + pub document_number: Option, + + /// Document issuing country + pub document_country: Option, + + /// Face match result + pub face_match: Option, + + /// Liveness check result + pub liveness_check: Option, + + /// Additional provider-specific data + #[serde(default)] + pub provider_data: std::collections::HashMap, +} + +impl KycSession { + /// Create a new KYC session + pub fn new(id: u32, client_id: String, provider: String) -> Self { + let mut base_data = BaseData::new(); + base_data.id = id; + Self { + base_data, + client_id, + provider, + session_token: None, + verification_url: None, + status: SessionStatus::Created, + expires_at: None, + callback_url: None, + success_url: None, + error_url: None, + locale: None, + provider_config: std::collections::HashMap::new(), + result: None, + } + } + + /// Builder: Set callback URL + pub fn callback_url(mut self, url: String) -> Self { + self.callback_url = Some(url); + self.base_data.update_modified(); + self + } + + /// Builder: Set success URL + pub fn success_url(mut self, url: String) -> Self { + self.success_url = Some(url); + self.base_data.update_modified(); + self + } + + /// Builder: Set error URL + pub fn error_url(mut self, url: String) -> Self { + self.error_url = Some(url); + self.base_data.update_modified(); + self + } + + /// Builder: Set locale + pub fn locale(mut self, locale: String) -> Self { + self.locale = Some(locale); + self.base_data.update_modified(); + self + } + + /// Set session token from provider + pub fn set_session_token(&mut self, token: String) { + self.session_token = Some(token); + self.base_data.update_modified(); + } + + /// Set verification URL + pub fn set_verification_url(&mut self, url: String) { + self.verification_url = Some(url); + self.base_data.update_modified(); + } + + /// Set session status + pub fn set_status(&mut self, status: SessionStatus) { + self.status = status; + self.base_data.update_modified(); + } + + /// Set expiration timestamp + pub fn set_expires_at(&mut self, timestamp: i64) { + self.expires_at = Some(timestamp); + self.base_data.update_modified(); + } + + /// Set session result + pub fn set_result(&mut self, result: SessionResult) { + self.result = Some(result); + self.base_data.update_modified(); + } + + /// Add provider-specific configuration + pub fn add_provider_config(&mut self, key: String, value: String) { + self.provider_config.insert(key, value); + self.base_data.update_modified(); + } +} diff --git a/src/objects/legal/contract.rs b/src/objects/legal/contract.rs new file mode 100644 index 0000000..5c1a0c1 --- /dev/null +++ b/src/objects/legal/contract.rs @@ -0,0 +1,129 @@ +/// Legal Contract Object +/// +/// Simple contract object with signatures for legal agreements + +use crate::store::{BaseData, Object}; +use serde::{Deserialize, Serialize}; + +/// Contract status +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ContractStatus { + Draft, + Active, + Completed, + Cancelled, +} + +impl Default for ContractStatus { + fn default() -> Self { + ContractStatus::Draft + } +} + +/// Legal contract with signatures +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)] +pub struct Contract { + /// Base data for object storage + pub base_data: BaseData, + + /// Contract title + pub title: String, + + /// Contract content/terms + pub content: String, + + /// Contract status + pub status: ContractStatus, + + /// List of signature IDs (references to Signature objects) + pub signatures: Vec, + + /// Creator user ID + pub creator_id: u32, + + /// Expiry timestamp (optional) + pub expires_at: Option, +} + +impl Contract { + /// Create a new contract + pub fn new(id: u32) -> Self { + let base_data = BaseData::with_id(id, String::new()); + Self { + base_data, + title: String::new(), + content: String::new(), + status: ContractStatus::default(), + signatures: Vec::new(), + creator_id: 0, + expires_at: None, + } + } + + /// Set the title (fluent) + pub fn title(mut self, title: impl ToString) -> Self { + self.title = title.to_string(); + self + } + + /// Set the content (fluent) + pub fn content(mut self, content: impl ToString) -> Self { + self.content = content.to_string(); + self + } + + /// Set the status (fluent) + pub fn status(mut self, status: ContractStatus) -> Self { + self.status = status; + self + } + + /// Set the creator ID (fluent) + pub fn creator_id(mut self, creator_id: u32) -> Self { + self.creator_id = creator_id; + self + } + + /// Set the expiry timestamp (fluent) + pub fn expires_at(mut self, expires_at: u64) -> Self { + self.expires_at = Some(expires_at); + self + } + + /// Add a signature (fluent) + pub fn add_signature(mut self, signature_id: u32) -> Self { + if !self.signatures.contains(&signature_id) { + self.signatures.push(signature_id); + } + self + } + + /// Remove a signature (fluent) + pub fn remove_signature(mut self, signature_id: u32) -> Self { + self.signatures.retain(|&id| id != signature_id); + self + } + + /// Check if all required signatures are present + pub fn is_fully_signed(&self, required_count: usize) -> bool { + self.signatures.len() >= required_count + } + + /// Activate the contract + pub fn activate(mut self) -> Self { + self.status = ContractStatus::Active; + self + } + + /// Complete the contract + pub fn complete(mut self) -> Self { + self.status = ContractStatus::Completed; + self + } + + /// Cancel the contract + pub fn cancel(mut self) -> Self { + self.status = ContractStatus::Cancelled; + self + } +} diff --git a/src/objects/legal/mod.rs b/src/objects/legal/mod.rs new file mode 100644 index 0000000..323fe5f --- /dev/null +++ b/src/objects/legal/mod.rs @@ -0,0 +1,7 @@ +/// Legal module for contracts and legal documents + +pub mod contract; +pub mod rhai; + +pub use contract::{Contract, ContractStatus}; +pub use rhai::register_legal_modules; diff --git a/src/objects/legal/rhai.rs b/src/objects/legal/rhai.rs new file mode 100644 index 0000000..f17774d --- /dev/null +++ b/src/objects/legal/rhai.rs @@ -0,0 +1,150 @@ +/// Rhai bindings for Legal objects (Contract) + +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; + +use super::{Contract, ContractStatus}; + +/// Register legal modules with the Rhai engine +pub fn register_legal_modules(parent_module: &mut Module) { + // Register custom types + parent_module.set_custom_type::("Contract"); + parent_module.set_custom_type::("ContractStatus"); + + // Merge contract functions + let contract_module = exported_module!(rhai_contract_module); + parent_module.merge(&contract_module); +} + +// ============================================================================ +// Contract Module +// ============================================================================ + +type RhaiContract = Contract; +type RhaiContractStatus = ContractStatus; + +#[export_module] +mod rhai_contract_module { + use super::{RhaiContract, RhaiContractStatus}; + use super::super::{Contract, ContractStatus}; + use ::rhai::EvalAltResult; + + // Contract constructor + #[rhai_fn(name = "new_contract", return_raw)] + pub fn new_contract(id: i64) -> Result> { + Ok(Contract::new(id as u32)) + } + + // Builder methods + #[rhai_fn(name = "title", return_raw)] + pub fn set_title( + contract: RhaiContract, + title: String, + ) -> Result> { + Ok(contract.title(title)) + } + + #[rhai_fn(name = "content", return_raw)] + pub fn set_content( + contract: RhaiContract, + content: String, + ) -> Result> { + Ok(contract.content(content)) + } + + #[rhai_fn(name = "creator_id", return_raw)] + pub fn set_creator_id( + contract: RhaiContract, + creator_id: i64, + ) -> Result> { + Ok(contract.creator_id(creator_id as u32)) + } + + #[rhai_fn(name = "expires_at", return_raw)] + pub fn set_expires_at( + contract: RhaiContract, + expires_at: i64, + ) -> Result> { + Ok(contract.expires_at(expires_at as u64)) + } + + #[rhai_fn(name = "add_signature", return_raw)] + pub fn add_signature( + contract: RhaiContract, + signature_id: i64, + ) -> Result> { + Ok(contract.add_signature(signature_id as u32)) + } + + #[rhai_fn(name = "remove_signature", return_raw)] + pub fn remove_signature( + contract: RhaiContract, + signature_id: i64, + ) -> Result> { + Ok(contract.remove_signature(signature_id as u32)) + } + + // State management methods + #[rhai_fn(name = "activate", return_raw)] + pub fn activate(contract: RhaiContract) -> Result> { + Ok(contract.activate()) + } + + #[rhai_fn(name = "complete", return_raw)] + pub fn complete(contract: RhaiContract) -> Result> { + Ok(contract.complete()) + } + + #[rhai_fn(name = "cancel", return_raw)] + pub fn cancel(contract: RhaiContract) -> Result> { + Ok(contract.cancel()) + } + + // Query methods + #[rhai_fn(name = "is_fully_signed", pure)] + pub fn is_fully_signed(contract: &mut RhaiContract, required_count: i64) -> bool { + contract.is_fully_signed(required_count as usize) + } + + // Getters + #[rhai_fn(name = "title", pure)] + pub fn get_title(contract: &mut RhaiContract) -> String { + contract.title.clone() + } + + #[rhai_fn(name = "content", pure)] + pub fn get_content(contract: &mut RhaiContract) -> String { + contract.content.clone() + } + + #[rhai_fn(name = "status", pure)] + pub fn get_status(contract: &mut RhaiContract) -> String { + format!("{:?}", contract.status) + } + + #[rhai_fn(name = "creator_id", pure)] + pub fn get_creator_id(contract: &mut RhaiContract) -> i64 { + contract.creator_id as i64 + } + + #[rhai_fn(name = "signature_count", pure)] + pub fn get_signature_count(contract: &mut RhaiContract) -> i64 { + contract.signatures.len() as i64 + } +} + +// ============================================================================ +// CustomType Implementations +// ============================================================================ + +impl CustomType for Contract { + fn build(mut builder: TypeBuilder) { + builder.with_name("Contract"); + } +} + +impl CustomType for ContractStatus { + fn build(mut builder: TypeBuilder) { + builder.with_name("ContractStatus"); + } +} diff --git a/src/objects/mod.rs b/src/objects/mod.rs index e06379f..6cb7409 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -1,5 +1,17 @@ pub mod note; pub mod event; +pub mod heroledger; +pub mod grid4; +pub mod kyc; +pub mod flow; +pub mod communication; +pub mod money; +pub mod legal; pub use note::Note; pub use event::Event; +pub use kyc::{KycInfo, KycSession}; +pub use flow::{FlowTemplate, FlowInstance}; +pub use communication::{Verification, EmailClient}; +pub use money::{Account, Asset, Transaction, PaymentClient}; +pub use legal::{Contract, ContractStatus}; diff --git a/src/objects/money/mod.rs b/src/objects/money/mod.rs new file mode 100644 index 0000000..b0d63ee --- /dev/null +++ b/src/objects/money/mod.rs @@ -0,0 +1,10 @@ +/// Money Module +/// +/// Financial objects including accounts, assets, transactions, and payment providers. + +pub mod models; +pub mod rhai; +pub mod payments; + +pub use models::{Account, Asset, Transaction, AccountStatus, TransactionType, Signature, AccountPolicyItem}; +pub use payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus}; diff --git a/src/objects/money/models.rs b/src/objects/money/models.rs new file mode 100644 index 0000000..55c4499 --- /dev/null +++ b/src/objects/money/models.rs @@ -0,0 +1,498 @@ +use crate::store::{BaseData, IndexKey, Object}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Represents the status of an account +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum AccountStatus { + Active, + Inactive, + Suspended, + Archived, +} + +impl Default for AccountStatus { + fn default() -> Self { + AccountStatus::Active + } +} + +/// Represents the type of transaction +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TransactionType { + Transfer, + Clawback, + Freeze, + Unfreeze, + Issue, + Burn, +} + +impl Default for TransactionType { + fn default() -> Self { + TransactionType::Transfer + } +} + +/// Represents a signature for transactions +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Signature { + pub signer_id: u32, + pub signature: String, + pub timestamp: u64, +} + +impl Signature { + pub fn new() -> Self { + Self { + signer_id: 0, + signature: String::new(), + timestamp: 0, + } + } + + pub fn signer_id(mut self, signer_id: u32) -> Self { + self.signer_id = signer_id; + self + } + + pub fn signature(mut self, signature: impl ToString) -> Self { + self.signature = signature.to_string(); + self + } + + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Policy item for account operations +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct AccountPolicyItem { + pub signers: Vec, + pub min_signatures: u32, + pub enabled: bool, + pub threshold: f64, + pub recipient: u32, +} + +impl AccountPolicyItem { + pub fn new() -> Self { + Self { + signers: Vec::new(), + min_signatures: 0, + enabled: false, + threshold: 0.0, + recipient: 0, + } + } + + pub fn add_signer(mut self, signer_id: u32) -> Self { + self.signers.push(signer_id); + self + } + + pub fn signers(mut self, signers: Vec) -> Self { + self.signers = signers; + self + } + + pub fn min_signatures(mut self, min_signatures: u32) -> Self { + self.min_signatures = min_signatures; + self + } + + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + pub fn threshold(mut self, threshold: f64) -> Self { + self.threshold = threshold; + self + } + + pub fn recipient(mut self, recipient: u32) -> Self { + self.recipient = recipient; + self + } + + pub fn build(self) -> Self { + self + } +} + +/// Represents an account in the financial system +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Account { + /// Base model data + pub base_data: BaseData, + pub owner_id: u32, + #[index] + pub address: String, + pub balance: f64, + pub currency: String, + pub assetid: u32, + pub last_activity: u64, + pub administrators: Vec, + pub accountpolicy: u32, +} + +impl Account { + /// Create a new account instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + owner_id: 0, + address: String::new(), + balance: 0.0, + currency: String::new(), + assetid: 0, + last_activity: 0, + administrators: Vec::new(), + accountpolicy: 0, + } + } + + /// Set the owner ID (fluent) + pub fn owner_id(mut self, owner_id: u32) -> Self { + self.owner_id = owner_id; + self + } + + /// Set the blockchain address (fluent) + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); + self + } + + /// Set the balance (fluent) + pub fn balance(mut self, balance: f64) -> Self { + self.balance = balance; + self + } + + /// Set the currency (fluent) + pub fn currency(mut self, currency: impl ToString) -> Self { + self.currency = currency.to_string(); + self + } + + /// Set the asset ID (fluent) + pub fn assetid(mut self, assetid: u32) -> Self { + self.assetid = assetid; + self + } + + /// Set the last activity timestamp (fluent) + pub fn last_activity(mut self, last_activity: u64) -> Self { + self.last_activity = last_activity; + self + } + + /// Add an administrator (fluent) + pub fn add_administrator(mut self, admin_id: u32) -> Self { + self.administrators.push(admin_id); + self + } + + /// Set all administrators (fluent) + pub fn administrators(mut self, administrators: Vec) -> Self { + self.administrators = administrators; + self + } + + /// Set the account policy ID (fluent) + pub fn accountpolicy(mut self, accountpolicy: u32) -> Self { + self.accountpolicy = accountpolicy; + self + } + + /// Build the final account instance + pub fn build(self) -> Self { + self + } +} + +/// Represents an asset in the financial system +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Asset { + /// Base model data + pub base_data: BaseData, + #[index] + pub address: String, + pub assetid: u32, + pub asset_type: String, + pub issuer: u32, + pub supply: f64, + pub decimals: u8, + pub is_frozen: bool, + pub metadata: HashMap, + pub administrators: Vec, + pub min_signatures: u32, +} + +impl Asset { + /// Create a new asset instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + address: String::new(), + assetid: 0, + asset_type: String::new(), + issuer: 0, + supply: 0.0, + decimals: 0, + is_frozen: false, + metadata: HashMap::new(), + administrators: Vec::new(), + min_signatures: 0, + } + } + + /// Set the blockchain address (fluent) + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); + self + } + + /// Set the asset ID (fluent) + pub fn assetid(mut self, assetid: u32) -> Self { + self.assetid = assetid; + self + } + + /// Set the asset type (fluent) + pub fn asset_type(mut self, asset_type: impl ToString) -> Self { + self.asset_type = asset_type.to_string(); + self + } + + /// Set the issuer (fluent) + pub fn issuer(mut self, issuer: u32) -> Self { + self.issuer = issuer; + self + } + + /// Set the supply (fluent) + pub fn supply(mut self, supply: f64) -> Self { + self.supply = supply; + self + } + + /// Set the decimals (fluent) + pub fn decimals(mut self, decimals: u8) -> Self { + self.decimals = decimals; + self + } + + /// Set the frozen status (fluent) + pub fn is_frozen(mut self, is_frozen: bool) -> Self { + self.is_frozen = is_frozen; + self + } + + /// Add metadata entry (fluent) + pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { + self.metadata.insert(key.to_string(), value.to_string()); + self + } + + /// Set all metadata (fluent) + pub fn metadata(mut self, metadata: HashMap) -> Self { + self.metadata = metadata; + self + } + + /// Add an administrator (fluent) + pub fn add_administrator(mut self, admin_id: u32) -> Self { + self.administrators.push(admin_id); + self + } + + /// Set all administrators (fluent) + pub fn administrators(mut self, administrators: Vec) -> Self { + self.administrators = administrators; + self + } + + /// Set minimum signatures required (fluent) + pub fn min_signatures(mut self, min_signatures: u32) -> Self { + self.min_signatures = min_signatures; + self + } + + /// Build the final asset instance + pub fn build(self) -> Self { + self + } +} + +/// Represents account policies for various operations +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct AccountPolicy { + /// Base model data + pub base_data: BaseData, + pub transferpolicy: AccountPolicyItem, + pub adminpolicy: AccountPolicyItem, + pub clawbackpolicy: AccountPolicyItem, + pub freezepolicy: AccountPolicyItem, +} + +impl AccountPolicy { + /// Create a new account policy instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + transferpolicy: AccountPolicyItem::new(), + adminpolicy: AccountPolicyItem::new(), + clawbackpolicy: AccountPolicyItem::new(), + freezepolicy: AccountPolicyItem::new(), + } + } + + /// Set the transfer policy (fluent) + pub fn transferpolicy(mut self, transferpolicy: AccountPolicyItem) -> Self { + self.transferpolicy = transferpolicy; + self + } + + /// Set the admin policy (fluent) + pub fn adminpolicy(mut self, adminpolicy: AccountPolicyItem) -> Self { + self.adminpolicy = adminpolicy; + self + } + + /// Set the clawback policy (fluent) + pub fn clawbackpolicy(mut self, clawbackpolicy: AccountPolicyItem) -> Self { + self.clawbackpolicy = clawbackpolicy; + self + } + + /// Set the freeze policy (fluent) + pub fn freezepolicy(mut self, freezepolicy: AccountPolicyItem) -> Self { + self.freezepolicy = freezepolicy; + self + } + + /// Build the final account policy instance + pub fn build(self) -> Self { + self + } +} + +/// Represents a financial transaction +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)] +pub struct Transaction { + /// Base model data + pub base_data: BaseData, + pub txid: u32, + pub source: u32, + pub destination: u32, + pub assetid: u32, + pub amount: f64, + pub timestamp: u64, + pub status: String, + pub memo: String, + pub tx_type: TransactionType, + pub signatures: Vec, +} + +impl Transaction { + /// Create a new transaction instance + pub fn new(id: u32) -> Self { + let mut base_data = BaseData::new(); + Self { + base_data, + txid: 0, + source: 0, + destination: 0, + assetid: 0, + amount: 0.0, + timestamp: 0, + status: String::new(), + memo: String::new(), + tx_type: TransactionType::default(), + signatures: Vec::new(), + } + } + + /// Set the transaction ID (fluent) + pub fn txid(mut self, txid: u32) -> Self { + self.txid = txid; + self + } + + /// Set the source account (fluent) + pub fn source(mut self, source: u32) -> Self { + self.source = source; + self + } + + /// Set the destination account (fluent) + pub fn destination(mut self, destination: u32) -> Self { + self.destination = destination; + self + } + + /// Set the asset ID (fluent) + pub fn assetid(mut self, assetid: u32) -> Self { + self.assetid = assetid; + self + } + + /// Set the amount (fluent) + pub fn amount(mut self, amount: f64) -> Self { + self.amount = amount; + self + } + + /// Set the timestamp (fluent) + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.timestamp = timestamp; + self + } + + /// Set the status (fluent) + pub fn status(mut self, status: impl ToString) -> Self { + self.status = status.to_string(); + self + } + + /// Set the memo (fluent) + pub fn memo(mut self, memo: impl ToString) -> Self { + self.memo = memo.to_string(); + self + } + + /// Set the transaction type (fluent) + pub fn tx_type(mut self, tx_type: TransactionType) -> Self { + self.tx_type = tx_type; + self + } + + /// Add a signature (fluent) + pub fn add_signature(mut self, signature: Signature) -> Self { + self.signatures.push(signature); + self + } + + /// Set all signatures (fluent) + pub fn signatures(mut self, signatures: Vec) -> Self { + self.signatures = signatures; + self + } + + /// Build the final transaction instance + pub fn build(self) -> Self { + self + } +} diff --git a/src/objects/money/payments.rs b/src/objects/money/payments.rs new file mode 100644 index 0000000..aa6cfa1 --- /dev/null +++ b/src/objects/money/payments.rs @@ -0,0 +1,457 @@ +/// Payment Provider Client +/// +/// Generic payment provider API client supporting multiple payment gateways. +/// Currently implements Pesapal API but designed to be extensible for other providers. + +use serde::{Deserialize, Serialize}; +use crate::store::{BaseData, IndexKey, Object}; + +// Helper to run async code synchronously +fn run_async(future: F) -> T +where + F: std::future::Future + Send + 'static, + T: Send + 'static, +{ + // Try to use current runtime handle if available + if tokio::runtime::Handle::try_current().is_ok() { + // We're in a runtime, spawn a blocking thread with its own runtime + std::thread::scope(|s| { + s.spawn(|| { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(future) + }).join().unwrap() + }) + } else { + // No runtime, create one + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(future) + } +} + +/// Payment Provider Client for making API calls to payment gateways +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentClient { + /// Base data for object storage + pub base_data: BaseData, + + /// Provider name (e.g., "pesapal", "stripe", "paypal", "flutterwave") + pub provider: String, + + /// Consumer key / API key + pub consumer_key: String, + + /// Consumer secret / API secret + pub consumer_secret: String, + + /// Base URL for API (optional, uses provider default if not set) + pub base_url: Option, + + /// Sandbox mode (for testing) + pub sandbox: bool, +} + +/// Payment request details +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentRequest { + /// Unique merchant reference + pub merchant_reference: String, + + /// Amount to charge + pub amount: f64, + + /// Currency code (e.g., "USD", "KES", "UGX") + pub currency: String, + + /// Description of the payment + pub description: String, + + /// Callback URL for payment notifications + pub callback_url: String, + + /// Redirect URL after payment (optional) + pub redirect_url: Option, + + /// Cancel URL (optional) + pub cancel_url: Option, + + /// Customer email + pub customer_email: Option, + + /// Customer phone + pub customer_phone: Option, + + /// Customer first name + pub customer_first_name: Option, + + /// Customer last name + pub customer_last_name: Option, +} + +/// Payment response from provider +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentResponse { + /// Payment link URL + pub payment_url: String, + + /// Order tracking ID from provider + pub order_tracking_id: String, + + /// Merchant reference + pub merchant_reference: String, + + /// Status message + pub status: String, +} + +/// Payment status query result +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaymentStatus { + /// Order tracking ID + pub order_tracking_id: String, + + /// Merchant reference + pub merchant_reference: String, + + /// Payment status (e.g., "PENDING", "COMPLETED", "FAILED") + pub status: String, + + /// Amount + pub amount: f64, + + /// Currency + pub currency: String, + + /// Payment method used + pub payment_method: Option, + + /// Transaction ID + pub transaction_id: Option, +} + +// Pesapal-specific structures +#[derive(Debug, Serialize)] +struct PesapalAuthRequest { + consumer_key: String, + consumer_secret: String, +} + +#[derive(Debug, Deserialize)] +struct PesapalAuthResponse { + token: String, + #[serde(rename = "expiryDate")] + expiry_date: Option, + error: Option, + status: Option, + message: Option, +} + +#[derive(Debug, Serialize)] +struct PesapalSubmitOrderRequest { + id: String, + currency: String, + amount: f64, + description: String, + callback_url: String, + redirect_mode: String, + notification_id: String, + billing_address: Option, +} + +#[derive(Debug, Serialize)] +struct PesapalBillingAddress { + email_address: Option, + phone_number: Option, + first_name: Option, + last_name: Option, +} + +#[derive(Debug, Deserialize)] +struct PesapalSubmitOrderResponse { + order_tracking_id: Option, + merchant_reference: Option, + redirect_url: Option, + error: Option, + status: Option, +} + +#[derive(Debug, Deserialize)] +struct PesapalTransactionStatusResponse { + payment_method: Option, + amount: f64, + created_date: String, + confirmation_code: Option, + payment_status_description: String, + description: String, + message: String, + payment_account: Option, + call_back_url: String, + status_code: i32, + merchant_reference: String, + payment_status_code: String, + currency: String, + error: Option, + status: String, +} + +impl PaymentClient { + /// Create a new payment client + pub fn new(id: u32, provider: String, consumer_key: String, consumer_secret: String) -> Self { + let base_data = BaseData::with_id(id, String::new()); + Self { + base_data, + provider, + consumer_key, + consumer_secret, + base_url: None, + sandbox: false, + } + } + + /// Create a Pesapal client + pub fn pesapal(id: u32, consumer_key: String, consumer_secret: String) -> Self { + let base_data = BaseData::with_id(id, String::new()); + Self { + base_data, + provider: "pesapal".to_string(), + consumer_key, + consumer_secret, + base_url: Some("https://pay.pesapal.com/v3".to_string()), + sandbox: false, + } + } + + /// Create a Pesapal sandbox client + pub fn pesapal_sandbox(id: u32, consumer_key: String, consumer_secret: String) -> Self { + let base_data = BaseData::with_id(id, String::new()); + Self { + base_data, + provider: "pesapal".to_string(), + consumer_key, + consumer_secret, + base_url: Some("https://cybqa.pesapal.com/pesapalv3".to_string()), + sandbox: true, + } + } + + /// Set custom base URL + pub fn with_base_url(mut self, base_url: String) -> Self { + self.base_url = Some(base_url); + self + } + + /// Enable sandbox mode + pub fn with_sandbox(mut self, sandbox: bool) -> Self { + self.sandbox = sandbox; + self + } + + /// Get the base URL for the provider + fn get_base_url(&self) -> String { + if let Some(url) = &self.base_url { + return url.clone(); + } + + match self.provider.as_str() { + "pesapal" => { + if self.sandbox { + "https://cybqa.pesapal.com/pesapalv3".to_string() + } else { + "https://pay.pesapal.com/v3".to_string() + } + } + "stripe" => "https://api.stripe.com/v1".to_string(), + "paypal" => "https://api.paypal.com/v2".to_string(), + "flutterwave" => "https://api.flutterwave.com/v3".to_string(), + _ => panic!("Unknown provider: {}", self.provider), + } + } + + /// Create a payment link + pub fn create_payment_link( + &self, + request: &PaymentRequest, + ) -> Result { + match self.provider.as_str() { + "pesapal" => self.create_pesapal_payment(request), + _ => Err(format!("Provider {} not yet implemented", self.provider)), + } + } + + /// Get payment status + pub fn get_payment_status( + &self, + order_tracking_id: &str, + ) -> Result { + match self.provider.as_str() { + "pesapal" => self.get_pesapal_status(order_tracking_id), + _ => Err(format!("Provider {} not yet implemented", self.provider)), + } + } + + /// Authenticate with Pesapal and get access token + fn pesapal_authenticate(&self) -> Result { + let url = format!("{}/api/Auth/RequestToken", self.get_base_url()); + + let auth_request = PesapalAuthRequest { + consumer_key: self.consumer_key.clone(), + consumer_secret: self.consumer_secret.clone(), + }; + + run_async(async move { + let client = reqwest::Client::new(); + let response = client + .post(&url) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .json(&auth_request) + .send() + .await + .map_err(|e| format!("Failed to send auth request: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(format!("Pesapal auth failed ({}): {}", status, error_text)); + } + + // Debug: print raw response + let response_text = response.text().await + .map_err(|e| format!("Failed to read auth response: {}", e))?; + println!("=== PESAPAL AUTH RESPONSE ==="); + println!("{}", response_text); + println!("=============================="); + + let auth_response: PesapalAuthResponse = serde_json::from_str(&response_text) + .map_err(|e| format!("Failed to parse auth response: {}", e))?; + + if let Some(error) = auth_response.error { + return Err(format!("Pesapal auth error: {}", error)); + } + + Ok(auth_response.token) + }) + } + + /// Create a Pesapal payment + fn create_pesapal_payment( + &self, + request: &PaymentRequest, + ) -> Result { + // Get auth token + let token = self.pesapal_authenticate()?; + + let url = format!("{}/api/Transactions/SubmitOrderRequest", self.get_base_url()); + + let pesapal_request = PesapalSubmitOrderRequest { + id: request.merchant_reference.clone(), + currency: request.currency.clone(), + amount: request.amount, + description: request.description.clone(), + callback_url: request.callback_url.clone(), + redirect_mode: String::new(), + notification_id: String::new(), + billing_address: Some(PesapalBillingAddress { + email_address: request.customer_email.clone(), + phone_number: request.customer_phone.clone(), + first_name: request.customer_first_name.clone(), + last_name: request.customer_last_name.clone(), + }), + }; + + run_async(async move { + let client = reqwest::Client::new(); + let response = client + .post(&url) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .bearer_auth(&token) + .json(&pesapal_request) + .send() + .await + .map_err(|e| format!("Failed to send payment request: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(format!("Pesapal payment request failed ({}): {}", status, error_text)); + } + + // Debug: print raw response + let response_text = response.text().await + .map_err(|e| format!("Failed to read payment response: {}", e))?; + println!("=== PESAPAL PAYMENT RESPONSE ==="); + println!("{}", response_text); + println!("================================="); + + let pesapal_response: PesapalSubmitOrderResponse = serde_json::from_str(&response_text) + .map_err(|e| format!("Failed to parse payment response: {}", e))?; + + if let Some(error) = pesapal_response.error { + return Err(format!("Pesapal payment error: {}", error)); + } + + Ok(PaymentResponse { + payment_url: pesapal_response.redirect_url.unwrap_or_default(), + order_tracking_id: pesapal_response.order_tracking_id.unwrap_or_default(), + merchant_reference: pesapal_response.merchant_reference.unwrap_or_default(), + status: pesapal_response.status.unwrap_or_default(), + }) + }) + } + + /// Get Pesapal payment status + fn get_pesapal_status( + &self, + order_tracking_id: &str, + ) -> Result { + let token = self.pesapal_authenticate()?; + let order_tracking_id = order_tracking_id.to_string(); + + let url = format!( + "{}/api/Transactions/GetTransactionStatus?orderTrackingId={}", + self.get_base_url(), + order_tracking_id + ); + + run_async(async move { + let client = reqwest::Client::new(); + let response = client + .get(&url) + .header("Accept", "application/json") + .bearer_auth(&token) + .send() + .await + .map_err(|e| format!("Failed to send status request: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(format!("Pesapal status request failed ({}): {}", status, error_text)); + } + + // Debug: print raw response + let response_text = response.text().await + .map_err(|e| format!("Failed to read status response: {}", e))?; + println!("=== PESAPAL STATUS RESPONSE ==="); + println!("{}", response_text); + println!("================================"); + + let status_response: PesapalTransactionStatusResponse = serde_json::from_str(&response_text) + .map_err(|e| format!("Failed to parse status response: {}", e))?; + + if let Some(error) = status_response.error { + return Err(format!("Pesapal status error: {}", error)); + } + + Ok(PaymentStatus { + order_tracking_id: order_tracking_id.to_string(), + merchant_reference: status_response.merchant_reference, + status: status_response.payment_status_description, + amount: status_response.amount, + currency: status_response.currency, + payment_method: status_response.payment_method, + transaction_id: status_response.confirmation_code, + }) + }) + } +} diff --git a/src/objects/money/rhai.rs b/src/objects/money/rhai.rs new file mode 100644 index 0000000..bf673d5 --- /dev/null +++ b/src/objects/money/rhai.rs @@ -0,0 +1,630 @@ +/// Rhai bindings for Money objects (Account, Asset, Transaction, PaymentClient) + +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; + +use super::models::{Account, Asset, Transaction}; +use super::payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus}; + +// ============================================================================ +// Account Module +// ============================================================================ + +type RhaiAccount = Account; + +#[export_module] +mod rhai_account_module { + use super::RhaiAccount; + + #[rhai_fn(name = "new_account", return_raw)] + pub fn new_account() -> Result> { + Ok(Account::new(0)) + } + + #[rhai_fn(name = "owner_id", return_raw)] + pub fn set_owner_id( + account: &mut RhaiAccount, + owner_id: i64, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.owner_id(owner_id as u32); + Ok(account.clone()) + } + + #[rhai_fn(name = "address", return_raw)] + pub fn set_address( + account: &mut RhaiAccount, + address: String, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.address(address); + Ok(account.clone()) + } + + #[rhai_fn(name = "balance", return_raw)] + pub fn set_balance( + account: &mut RhaiAccount, + balance: f64, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.balance(balance); + Ok(account.clone()) + } + + #[rhai_fn(name = "currency", return_raw)] + pub fn set_currency( + account: &mut RhaiAccount, + currency: String, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.currency(currency); + Ok(account.clone()) + } + + #[rhai_fn(name = "assetid", return_raw)] + pub fn set_assetid( + account: &mut RhaiAccount, + assetid: i64, + ) -> Result> { + let owned = std::mem::take(account); + *account = owned.assetid(assetid as u32); + Ok(account.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(account: &mut RhaiAccount) -> i64 { + account.base_data.id as i64 + } + + #[rhai_fn(name = "get_owner_id")] + pub fn get_owner_id(account: &mut RhaiAccount) -> i64 { + account.owner_id as i64 + } + + #[rhai_fn(name = "get_address")] + pub fn get_address(account: &mut RhaiAccount) -> String { + account.address.clone() + } + + #[rhai_fn(name = "get_balance")] + pub fn get_balance(account: &mut RhaiAccount) -> f64 { + account.balance + } + + #[rhai_fn(name = "get_currency")] + pub fn get_currency(account: &mut RhaiAccount) -> String { + account.currency.clone() + } +} + +// ============================================================================ +// Asset Module +// ============================================================================ + +type RhaiAsset = Asset; + +#[export_module] +mod rhai_asset_module { + use super::RhaiAsset; + + #[rhai_fn(name = "new_asset", return_raw)] + pub fn new_asset() -> Result> { + Ok(Asset::new(0)) + } + + #[rhai_fn(name = "address", return_raw)] + pub fn set_address( + asset: &mut RhaiAsset, + address: String, + ) -> Result> { + let owned = std::mem::take(asset); + *asset = owned.address(address); + Ok(asset.clone()) + } + + #[rhai_fn(name = "asset_type", return_raw)] + pub fn set_asset_type( + asset: &mut RhaiAsset, + asset_type: String, + ) -> Result> { + let owned = std::mem::take(asset); + *asset = owned.asset_type(asset_type); + Ok(asset.clone()) + } + + #[rhai_fn(name = "issuer", return_raw)] + pub fn set_issuer( + asset: &mut RhaiAsset, + issuer: i64, + ) -> Result> { + let owned = std::mem::take(asset); + *asset = owned.issuer(issuer as u32); + Ok(asset.clone()) + } + + #[rhai_fn(name = "supply", return_raw)] + pub fn set_supply( + asset: &mut RhaiAsset, + supply: f64, + ) -> Result> { + let owned = std::mem::take(asset); + *asset = owned.supply(supply); + Ok(asset.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(asset: &mut RhaiAsset) -> i64 { + asset.base_data.id as i64 + } + + #[rhai_fn(name = "get_address")] + pub fn get_address(asset: &mut RhaiAsset) -> String { + asset.address.clone() + } + + #[rhai_fn(name = "get_asset_type")] + pub fn get_asset_type(asset: &mut RhaiAsset) -> String { + asset.asset_type.clone() + } + + #[rhai_fn(name = "get_supply")] + pub fn get_supply(asset: &mut RhaiAsset) -> f64 { + asset.supply + } +} + +// ============================================================================ +// Transaction Module +// ============================================================================ + +type RhaiTransaction = Transaction; + +#[export_module] +mod rhai_transaction_module { + use super::RhaiTransaction; + + #[rhai_fn(name = "new_transaction", return_raw)] + pub fn new_transaction() -> Result> { + Ok(Transaction::new(0)) + } + + #[rhai_fn(name = "source", return_raw)] + pub fn set_source( + tx: &mut RhaiTransaction, + source: i64, + ) -> Result> { + let owned = std::mem::take(tx); + *tx = owned.source(source as u32); + Ok(tx.clone()) + } + + #[rhai_fn(name = "destination", return_raw)] + pub fn set_destination( + tx: &mut RhaiTransaction, + destination: i64, + ) -> Result> { + let owned = std::mem::take(tx); + *tx = owned.destination(destination as u32); + Ok(tx.clone()) + } + + #[rhai_fn(name = "amount", return_raw)] + pub fn set_amount( + tx: &mut RhaiTransaction, + amount: f64, + ) -> Result> { + let owned = std::mem::take(tx); + *tx = owned.amount(amount); + Ok(tx.clone()) + } + + #[rhai_fn(name = "assetid", return_raw)] + pub fn set_assetid( + tx: &mut RhaiTransaction, + assetid: i64, + ) -> Result> { + let owned = std::mem::take(tx); + *tx = owned.assetid(assetid as u32); + Ok(tx.clone()) + } + + // Getters + #[rhai_fn(name = "get_id")] + pub fn get_id(tx: &mut RhaiTransaction) -> i64 { + tx.base_data.id as i64 + } + + #[rhai_fn(name = "get_source")] + pub fn get_source(tx: &mut RhaiTransaction) -> i64 { + tx.source as i64 + } + + #[rhai_fn(name = "get_destination")] + pub fn get_destination(tx: &mut RhaiTransaction) -> i64 { + tx.destination as i64 + } + + #[rhai_fn(name = "get_amount")] + pub fn get_amount(tx: &mut RhaiTransaction) -> f64 { + tx.amount + } + + #[rhai_fn(name = "get_assetid")] + pub fn get_assetid(tx: &mut RhaiTransaction) -> i64 { + tx.assetid as i64 + } +} + +// ============================================================================ +// Registration Functions +// ============================================================================ + +/// Register money modules with the Rhai engine +pub fn register_money_modules(parent_module: &mut Module) { + // Register custom types + parent_module.set_custom_type::("Account"); + parent_module.set_custom_type::("Asset"); + parent_module.set_custom_type::("Transaction"); + parent_module.set_custom_type::("PaymentClient"); + parent_module.set_custom_type::("PaymentRequest"); + parent_module.set_custom_type::("PaymentResponse"); + parent_module.set_custom_type::("PaymentStatus"); + + // Merge account functions + let account_module = exported_module!(rhai_account_module); + parent_module.merge(&account_module); + + // Merge asset functions + let asset_module = exported_module!(rhai_asset_module); + parent_module.merge(&asset_module); + + // Merge transaction functions + let transaction_module = exported_module!(rhai_transaction_module); + parent_module.merge(&transaction_module); + + // Merge payment client functions + let payment_module = exported_module!(rhai_payment_module); + parent_module.merge(&payment_module); + + // Merge ethereum wallet functions + let eth_module = exported_module!(rhai_ethereum_module); + parent_module.merge(ð_module); +} + +// ============================================================================ +// Payment Provider Module +// ============================================================================ + +type RhaiPaymentClient = PaymentClient; +type RhaiPaymentRequest = PaymentRequest; +type RhaiPaymentResponse = PaymentResponse; +type RhaiPaymentStatus = PaymentStatus; + +#[export_module] +mod rhai_payment_module { + use super::{RhaiPaymentClient, RhaiPaymentRequest, RhaiPaymentResponse, RhaiPaymentStatus}; + use super::super::payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus}; + use ::rhai::EvalAltResult; + + // PaymentClient constructors + #[rhai_fn(name = "new_payment_client_pesapal", return_raw)] + pub fn new_pesapal_client( + id: i64, + consumer_key: String, + consumer_secret: String, + ) -> Result> { + Ok(PaymentClient::pesapal(id as u32, consumer_key, consumer_secret)) + } + + #[rhai_fn(name = "new_payment_client_pesapal_sandbox", return_raw)] + pub fn new_pesapal_sandbox_client( + id: i64, + consumer_key: String, + consumer_secret: String, + ) -> Result> { + Ok(PaymentClient::pesapal_sandbox(id as u32, consumer_key, consumer_secret)) + } + + // PaymentRequest constructor and builder methods + #[rhai_fn(name = "new_payment_request", return_raw)] + pub fn new_payment_request() -> Result> { + Ok(PaymentRequest { + merchant_reference: String::new(), + amount: 0.0, + currency: String::from("USD"), + description: String::new(), + callback_url: String::new(), + redirect_url: None, + cancel_url: None, + customer_email: None, + customer_phone: None, + customer_first_name: None, + customer_last_name: None, + }) + } + + #[rhai_fn(name = "amount", return_raw)] + pub fn set_amount( + request: &mut RhaiPaymentRequest, + amount: f64, + ) -> Result> { + request.amount = amount; + Ok(request.clone()) + } + + #[rhai_fn(name = "currency", return_raw)] + pub fn set_currency( + request: &mut RhaiPaymentRequest, + currency: String, + ) -> Result> { + request.currency = currency; + Ok(request.clone()) + } + + #[rhai_fn(name = "description", return_raw)] + pub fn set_description( + request: &mut RhaiPaymentRequest, + description: String, + ) -> Result> { + request.description = description; + Ok(request.clone()) + } + + #[rhai_fn(name = "callback_url", return_raw)] + pub fn set_callback_url( + request: &mut RhaiPaymentRequest, + url: String, + ) -> Result> { + request.callback_url = url; + Ok(request.clone()) + } + + #[rhai_fn(name = "merchant_reference", return_raw)] + pub fn set_merchant_reference( + request: &mut RhaiPaymentRequest, + reference: String, + ) -> Result> { + request.merchant_reference = reference; + Ok(request.clone()) + } + + #[rhai_fn(name = "customer_email", return_raw)] + pub fn set_customer_email( + request: &mut RhaiPaymentRequest, + email: String, + ) -> Result> { + request.customer_email = Some(email); + Ok(request.clone()) + } + + #[rhai_fn(name = "customer_phone", return_raw)] + pub fn set_customer_phone( + request: &mut RhaiPaymentRequest, + phone: String, + ) -> Result> { + request.customer_phone = Some(phone); + Ok(request.clone()) + } + + #[rhai_fn(name = "customer_name", return_raw)] + pub fn set_customer_name( + request: &mut RhaiPaymentRequest, + first_name: String, + last_name: String, + ) -> Result> { + request.customer_first_name = Some(first_name); + request.customer_last_name = Some(last_name); + Ok(request.clone()) + } + + #[rhai_fn(name = "redirect_url", return_raw)] + pub fn set_redirect_url( + request: &mut RhaiPaymentRequest, + url: String, + ) -> Result> { + request.redirect_url = Some(url); + Ok(request.clone()) + } + + // PaymentClient methods + #[rhai_fn(name = "create_payment_link", return_raw)] + pub fn create_payment_link( + client: &mut RhaiPaymentClient, + request: RhaiPaymentRequest, + ) -> Result> { + client.create_payment_link(&request) + .map_err(|e| e.into()) + } + + #[rhai_fn(name = "get_payment_status", return_raw)] + pub fn get_payment_status( + client: &mut RhaiPaymentClient, + order_tracking_id: String, + ) -> Result> { + client.get_payment_status(&order_tracking_id) + .map_err(|e| e.into()) + } + + // PaymentResponse getters + #[rhai_fn(name = "get_payment_url", pure)] + pub fn get_payment_url(response: &mut RhaiPaymentResponse) -> String { + response.payment_url.clone() + } + + #[rhai_fn(name = "get_order_tracking_id", pure)] + pub fn get_order_tracking_id(response: &mut RhaiPaymentResponse) -> String { + response.order_tracking_id.clone() + } + + #[rhai_fn(name = "get_merchant_reference", pure)] + pub fn get_merchant_reference(response: &mut RhaiPaymentResponse) -> String { + response.merchant_reference.clone() + } + + #[rhai_fn(name = "get_status", pure)] + pub fn get_response_status(response: &mut RhaiPaymentResponse) -> String { + response.status.clone() + } + + // PaymentStatus getters + #[rhai_fn(name = "get_status", pure)] + pub fn get_payment_status_value(status: &mut RhaiPaymentStatus) -> String { + status.status.clone() + } + + #[rhai_fn(name = "get_amount", pure)] + pub fn get_amount(status: &mut RhaiPaymentStatus) -> f64 { + status.amount + } + + #[rhai_fn(name = "get_currency", pure)] + pub fn get_currency(status: &mut RhaiPaymentStatus) -> String { + status.currency.clone() + } + + #[rhai_fn(name = "get_payment_method", pure)] + pub fn get_payment_method(status: &mut RhaiPaymentStatus) -> String { + status.payment_method.clone().unwrap_or_default() + } + + #[rhai_fn(name = "get_transaction_id", pure)] + pub fn get_transaction_id(status: &mut RhaiPaymentStatus) -> String { + status.transaction_id.clone().unwrap_or_default() + } +} + +// ============================================================================ +// CustomType Implementations +// ============================================================================ + +impl CustomType for Account { + fn build(mut builder: TypeBuilder) { + builder.with_name("Account"); + } +} + +impl CustomType for Asset { + fn build(mut builder: TypeBuilder) { + builder.with_name("Asset"); + } +} + +impl CustomType for Transaction { + fn build(mut builder: TypeBuilder) { + builder.with_name("Transaction"); + } +} + +impl CustomType for PaymentClient { + fn build(mut builder: TypeBuilder) { + builder.with_name("PaymentClient"); + } +} + +impl CustomType for PaymentRequest { + fn build(mut builder: TypeBuilder) { + builder.with_name("PaymentRequest"); + } +} + +impl CustomType for PaymentResponse { + fn build(mut builder: TypeBuilder) { + builder.with_name("PaymentResponse"); + } +} + +impl CustomType for PaymentStatus { + fn build(mut builder: TypeBuilder) { + builder.with_name("PaymentStatus"); + } +} + +// ============================================================================ +// Ethereum Wallet Module (Stub Implementation) +// ============================================================================ + +/// Simple Ethereum wallet representation +#[derive(Debug, Clone, Default)] +pub struct EthereumWallet { + pub owner_id: u32, + pub address: String, + pub network: String, +} + +impl EthereumWallet { + pub fn new() -> Self { + // Generate a mock Ethereum address (in production, use ethers-rs or similar) + use std::time::{SystemTime, UNIX_EPOCH}; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let mock_address = format!("0x{:040x}", timestamp as u128); + Self { + owner_id: 0, + address: mock_address, + network: String::from("mainnet"), + } + } + + pub fn owner_id(mut self, id: u32) -> Self { + self.owner_id = id; + self + } + + pub fn network(mut self, network: impl ToString) -> Self { + self.network = network.to_string(); + self + } +} + +type RhaiEthereumWallet = EthereumWallet; + +#[export_module] +mod rhai_ethereum_module { + use super::RhaiEthereumWallet; + use ::rhai::EvalAltResult; + + #[rhai_fn(name = "new_ethereum_wallet", return_raw)] + pub fn new_ethereum_wallet() -> Result> { + Ok(EthereumWallet::new()) + } + + #[rhai_fn(name = "owner_id", return_raw)] + pub fn set_owner_id( + wallet: &mut RhaiEthereumWallet, + owner_id: i64, + ) -> Result> { + let owned = std::mem::take(wallet); + *wallet = owned.owner_id(owner_id as u32); + Ok(wallet.clone()) + } + + #[rhai_fn(name = "network", return_raw)] + pub fn set_network( + wallet: &mut RhaiEthereumWallet, + network: String, + ) -> Result> { + let owned = std::mem::take(wallet); + *wallet = owned.network(network); + Ok(wallet.clone()) + } + + #[rhai_fn(name = "get_address")] + pub fn get_address(wallet: &mut RhaiEthereumWallet) -> String { + wallet.address.clone() + } + + #[rhai_fn(name = "get_network")] + pub fn get_network(wallet: &mut RhaiEthereumWallet) -> String { + wallet.network.clone() + } +} + +impl CustomType for EthereumWallet { + fn build(mut builder: TypeBuilder) { + builder.with_name("EthereumWallet"); + } +} diff --git a/src/objects/note/mod.rs b/src/objects/note/mod.rs index d8dd194..b5f1fe9 100644 --- a/src/objects/note/mod.rs +++ b/src/objects/note/mod.rs @@ -26,7 +26,7 @@ impl Note { /// Create a new note pub fn new(ns: String) -> Self { Self { - base_data: BaseData::new(ns), + base_data: BaseData::with_ns(ns), title: None, content: None, tags: BTreeMap::new(), @@ -35,8 +35,9 @@ impl Note { /// Create a note with specific ID pub fn with_id(id: String, ns: String) -> Self { + let id_u32 = id.parse::().unwrap_or(0); Self { - base_data: BaseData::with_id(id, ns), + base_data: BaseData::with_id(id_u32, ns), title: None, content: None, tags: BTreeMap::new(), diff --git a/src/store/base_data.rs b/src/store/base_data.rs index 99b3649..b94e7b9 100644 --- a/src/store/base_data.rs +++ b/src/store/base_data.rs @@ -6,7 +6,7 @@ use time::OffsetDateTime; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct BaseData { /// Unique ID (auto-generated or user-assigned) - pub id: String, + pub id: u32, /// Namespace this object belongs to pub ns: String, @@ -27,12 +27,25 @@ pub struct BaseData { } impl BaseData { - /// Create new base data with generated UUID - pub fn new(ns: String) -> Self { + /// Create new base data with ID 0 (no namespace required) + pub fn new() -> Self { let now = OffsetDateTime::now_utc(); Self { - id: uuid::Uuid::new_v4().to_string(), - ns, + id: 0, + ns: String::new(), + created_at: now, + modified_at: now, + mime: None, + size: None, + } + } + + /// Create new base data with namespace + pub fn with_ns(ns: impl ToString) -> Self { + let now = OffsetDateTime::now_utc(); + Self { + id: 0, + ns: ns.to_string(), created_at: now, modified_at: now, mime: None, @@ -41,7 +54,7 @@ impl BaseData { } /// Create new base data with specific ID - pub fn with_id(id: String, ns: String) -> Self { + pub fn with_id(id: u32, ns: String) -> Self { let now = OffsetDateTime::now_utc(); Self { id, @@ -73,6 +86,6 @@ impl BaseData { impl Default for BaseData { fn default() -> Self { - Self::new(String::from("default")) + Self::new() } } diff --git a/src/store/generic_store.rs b/src/store/generic_store.rs index 669e163..36e0a0b 100644 --- a/src/store/generic_store.rs +++ b/src/store/generic_store.rs @@ -73,12 +73,12 @@ impl GenericStore { for key in index_keys { let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value); - self.client.sadd(&field_key, obj.id()).await?; + self.client.sadd(&field_key, &obj.id().to_string()).await?; } // Add to scan index for full-text search let scan_key = format!("scan:{}", obj.namespace()); - self.client.sadd(&scan_key, obj.id()).await?; + self.client.sadd(&scan_key, &obj.id().to_string()).await?; Ok(()) } @@ -89,12 +89,12 @@ impl GenericStore { for key in index_keys { let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value); - self.client.srem(&field_key, obj.id()).await?; + self.client.srem(&field_key, &obj.id().to_string()).await?; } // Remove from scan index let scan_key = format!("scan:{}", obj.namespace()); - self.client.srem(&scan_key, obj.id()).await?; + self.client.srem(&scan_key, &obj.id().to_string()).await?; Ok(()) } diff --git a/src/store/object_trait.rs b/src/store/object_trait.rs index ce4719c..fd75ed3 100644 --- a/src/store/object_trait.rs +++ b/src/store/object_trait.rs @@ -37,13 +37,13 @@ pub trait Object: Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + fn base_data_mut(&mut self) -> &mut BaseData; /// Get the unique ID for this object - fn id(&self) -> &str { - &self.base_data().id + fn id(&self) -> u32 { + self.base_data().id } /// Set the unique ID for this object - fn set_id(&mut self, id: impl ToString) { - self.base_data_mut().id = id.to_string(); + fn set_id(&mut self, id: u32) { + self.base_data_mut().id = id; } /// Get the namespace for this object