diff --git a/server/tests/mod.rs b/server/tests/mod.rs index 497c490..09829a3 100644 --- a/server/tests/mod.rs +++ b/server/tests/mod.rs @@ -1,4 +1,4 @@ // tests/mod.rs pub mod tables_data; pub mod common; -pub mod table_definition; +// pub mod table_definition; diff --git a/server/tests/tables_data/mod.rs b/server/tests/tables_data/mod.rs index 26f047e..ae15c80 100644 --- a/server/tests/tables_data/mod.rs +++ b/server/tests/tables_data/mod.rs @@ -1,6 +1,6 @@ // tests/tables_data/mod.rs -pub mod get; -pub mod delete; +// pub mod get; +// pub mod delete; pub mod post; -pub mod put; +// pub mod put; diff --git a/server/tests/tables_data/post/mod.rs b/server/tests/tables_data/post/mod.rs index 5701d71..5980e9a 100644 --- a/server/tests/tables_data/post/mod.rs +++ b/server/tests/tables_data/post/mod.rs @@ -1,3 +1,4 @@ // tests/tables_data/post/mod.rs -pub mod post_table_data_test; +// pub mod post_table_data_test; +pub mod post_table_data_steel_decimal_test; diff --git a/server/tests/tables_data/post/post_table_data_steel_decimal_test.rs b/server/tests/tables_data/post/post_table_data_steel_decimal_test.rs new file mode 100644 index 0000000..2d09bc8 --- /dev/null +++ b/server/tests/tables_data/post/post_table_data_steel_decimal_test.rs @@ -0,0 +1,968 @@ +// tests/tables_data/post/post_table_data_steel_decimal_test.rs + +use rstest::{fixture, rstest}; +use sqlx::PgPool; +use sqlx::Row; +use std::collections::HashMap; +use prost_types::Value; +use prost_types::value::Kind; +use common::proto::multieko2::tables_data::PostTableDataRequest; +use common::proto::multieko2::table_definition::{ + PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition +}; +use server::tables_data::handlers::post_table_data; +use server::table_definition::handlers::post_table_definition; +use crate::common::setup_test_db; +use tonic::Status; +use tokio::sync::mpsc; +use server::indexer::IndexCommand; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use std::str::FromStr; +use rand::distr::Alphanumeric; +use rand::Rng; + +// Helper function to generate unique identifiers for test isolation +fn generate_unique_id() -> String { + rand::rng() + .sample_iter(&Alphanumeric) + .take(8) + .map(char::from) + .collect::() + .to_lowercase() +} + +// Helper to create a protobuf Value from a string +fn proto_string(s: &str) -> Value { + Value { + kind: Some(Kind::StringValue(s.to_string())), + } +} + +// Helper to create a protobuf Value from a number +fn proto_number(n: f64) -> Value { + Value { + kind: Some(Kind::NumberValue(n)), + } +} + +// Test context for Steel decimal integration tests +#[derive(Clone)] +struct SteelDecimalTestContext { + pool: PgPool, + profile_name: String, + invoice_table: String, + calculation_table: String, + validation_table: String, +} + +// Create tables with Steel scripts that use decimal operations +async fn create_steel_decimal_test_tables( + pool: &PgPool, + profile_name: &str, +) -> Result { + let unique_id = generate_unique_id(); + let invoice_table = format!("invoice_steel_{}", unique_id); + let calculation_table = format!("calc_steel_{}", unique_id); + let validation_table = format!("valid_steel_{}", unique_id); + + // Table 1: Invoice table with price calculation script + let invoice_def = PostTableDefinitionRequest { + profile_name: profile_name.into(), + table_name: invoice_table.clone(), + columns: vec![ + TableColumnDefinition { name: "item_name".into(), field_type: "text".into() }, + TableColumnDefinition { name: "quantity".into(), field_type: "integer".into() }, + TableColumnDefinition { name: "unit_price".into(), field_type: "decimal(10,2)".into() }, + TableColumnDefinition { name: "total_price".into(), field_type: "decimal(10,2)".into() }, + TableColumnDefinition { name: "tax_rate".into(), field_type: "decimal(5,4)".into() }, + TableColumnDefinition { name: "final_amount".into(), field_type: "decimal(12,2)".into() }, + ], + indexes: vec![], + links: vec![], + }; + post_table_definition(pool, invoice_def).await?; + + // Table 2: Calculation table with complex decimal arithmetic + let calc_def = PostTableDefinitionRequest { + profile_name: profile_name.into(), + table_name: calculation_table.clone(), + columns: vec![ + TableColumnDefinition { name: "operation_name".into(), field_type: "text".into() }, + TableColumnDefinition { name: "input_a".into(), field_type: "decimal(15,5)".into() }, + TableColumnDefinition { name: "input_b".into(), field_type: "decimal(15,5)".into() }, + TableColumnDefinition { name: "result".into(), field_type: "decimal(20,5)".into() }, + TableColumnDefinition { name: "precision_test".into(), field_type: "decimal(30,10)".into() }, + ], + indexes: vec![], + links: vec![], + }; + post_table_definition(pool, calc_def).await?; + + // Table 3: Validation table with business logic scripts + let validation_def = PostTableDefinitionRequest { + profile_name: profile_name.into(), + table_name: validation_table.clone(), + columns: vec![ + TableColumnDefinition { name: "product_code".into(), field_type: "text".into() }, + TableColumnDefinition { name: "price".into(), field_type: "decimal(10,2)".into() }, + TableColumnDefinition { name: "discount_percent".into(), field_type: "decimal(5,2)".into() }, + TableColumnDefinition { name: "discounted_price".into(), field_type: "decimal(10,2)".into() }, + TableColumnDefinition { name: "margin_check".into(), field_type: "text".into() }, + ], + indexes: vec![], + links: vec![], + }; + post_table_definition(pool, validation_def).await?; + + // Insert Steel scripts that use decimal operations + + // Script 1: Calculate total price (quantity * unit_price) + let total_price_script = r#" +(define (calculate-total-price quantity unit-price) + (steel/decimal/multiply + (steel/decimal/from-string quantity) + (steel/decimal/from-string unit-price))) + +(steel/decimal/to-string + (calculate-total-price + (hash-get row-data "quantity") + (hash-get row-data "unit_price"))) +"#; + + // Script 2: Calculate final amount with tax (total_price * (1 + tax_rate)) + let final_amount_script = r#" +(define (calculate-final-amount total-price tax-rate) + (let ([total-decimal (steel/decimal/from-string total-price)] + [tax-decimal (steel/decimal/from-string tax-rate)] + [one (steel/decimal/from-string "1")]) + (steel/decimal/multiply + total-decimal + (steel/decimal/add one tax-decimal)))) + +(steel/decimal/to-string + (calculate-final-amount + (hash-get row-data "total_price") + (hash-get row-data "tax_rate"))) +"#; + + // Script 3: Complex arithmetic result (input_a + input_b) * sqrt(input_a) + let arithmetic_script = r#" +(define (complex-calculation a b) + (let ([a-decimal (steel/decimal/from-string a)] + [b-decimal (steel/decimal/from-string b)]) + (let ([sum (steel/decimal/add a-decimal b-decimal)] + [sqrt-a (steel/decimal/sqrt a-decimal)]) + (steel/decimal/multiply sum sqrt-a)))) + +(steel/decimal/to-string + (complex-calculation + (hash-get row-data "input_a") + (hash-get row-data "input_b"))) +"#; + + // Script 4: High precision calculation + let precision_script = r#" +(define (high-precision-calc a b) + (let ([a-decimal (steel/decimal/from-string a)] + [b-decimal (steel/decimal/from-string b)] + [pi (steel/decimal/from-string "3.1415926535")]) + (steel/decimal/multiply + (steel/decimal/divide a-decimal b-decimal) + pi))) + +(steel/decimal/to-string + (high-precision-calc + (hash-get row-data "input_a") + (hash-get row-data "input_b"))) +"#; + + // Script 5: Calculate discounted price (price * (1 - discount_percent/100)) + let discount_script = r#" +(define (calculate-discount price discount-percent) + (let ([price-decimal (steel/decimal/from-string price)] + [discount-decimal (steel/decimal/from-string discount-percent)] + [hundred (steel/decimal/from-string "100")] + [one (steel/decimal/from-string "1")]) + (let ([discount-ratio (steel/decimal/divide discount-decimal hundred)] + [multiplier (steel/decimal/subtract one discount-ratio)]) + (steel/decimal/multiply price-decimal multiplier)))) + +(steel/decimal/to-string + (calculate-discount + (hash-get row-data "price") + (hash-get row-data "discount_percent"))) +"#; + + // Script 6: Margin validation check + let margin_check_script = r#" +(define (check-margin price discounted-price) + (let ([price-decimal (steel/decimal/from-string price)] + [discounted-decimal (steel/decimal/from-string discounted-price)] + [min-margin (steel/decimal/from-string "0.20")]) + (let ([difference (steel/decimal/subtract price-decimal discounted-decimal)] + [margin-ratio (steel/decimal/divide difference price-decimal)]) + (if (steel/decimal/greater-than? margin-ratio min-margin) + "ACCEPTABLE" + "TOO_LOW")))) + +(check-margin + (hash-get row-data "price") + (hash-get row-data "discounted_price")) +"#; + + // Get table definition IDs for script insertion + let invoice_table_id = sqlx::query_scalar!( + "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", + profile_name, + invoice_table + ) + .fetch_one(pool) + .await + .map_err(|e| Status::internal(format!("Failed to get invoice table ID: {}", e)))?; + + let calc_table_id = sqlx::query_scalar!( + "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", + profile_name, + calculation_table + ) + .fetch_one(pool) + .await + .map_err(|e| Status::internal(format!("Failed to get calc table ID: {}", e)))?; + + let validation_table_id = sqlx::query_scalar!( + "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", + profile_name, + validation_table + ) + .fetch_one(pool) + .await + .map_err(|e| Status::internal(format!("Failed to get validation table ID: {}", e)))?; + + // Get the schema_id for script insertion + let schema_id = sqlx::query_scalar!( + "SELECT id FROM schemas WHERE name = $1", + profile_name + ) + .fetch_one(pool) + .await + .map_err(|e| Status::internal(format!("Failed to get schema ID: {}", e)))?; + + // Insert scripts into table_scripts table with all required columns + let scripts = vec![ + (invoice_table_id, &invoice_table, "total_price", "NUMERIC(10,2)", total_price_script, "Calculate total price (quantity * unit_price)"), + (invoice_table_id, &invoice_table, "final_amount", "NUMERIC(12,2)", final_amount_script, "Calculate final amount with tax"), + (calc_table_id, &calculation_table, "result", "NUMERIC(20,5)", arithmetic_script, "Complex arithmetic calculation"), + (calc_table_id, &calculation_table, "precision_test", "NUMERIC(30,10)", precision_script, "High precision calculation"), + (validation_table_id, &validation_table, "discounted_price", "NUMERIC(10,2)", discount_script, "Calculate discounted price"), + (validation_table_id, &validation_table, "margin_check", "TEXT", margin_check_script, "Validate margin requirements"), + ]; + + for (table_id, target_table, target_column, target_column_type, script, description) in scripts { + sqlx::query!( + "INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + table_id, + target_table, + target_column, + target_column_type, + script, + description, + schema_id + ) + .execute(pool) + .await + .map_err(|e| Status::internal(format!("Failed to insert script: {}", e)))?; + } + + Ok(SteelDecimalTestContext { + pool: pool.clone(), + profile_name: profile_name.to_string(), + invoice_table, + calculation_table, + validation_table, + }) +} + +async fn create_test_indexer_channel() -> mpsc::Sender { + let (tx, mut rx) = mpsc::channel(100); + + // Spawn a task to consume indexer messages to prevent blocking + tokio::spawn(async move { + while let Some(_) = rx.recv().await { + // Just consume the messages + } + }); + + tx +} + +#[fixture] +async fn steel_decimal_context() -> SteelDecimalTestContext { + let pool = setup_test_db().await; + let unique_id = generate_unique_id(); + let profile_name = format!("steel_decimal_profile_{}", unique_id); + + create_steel_decimal_test_tables(&pool, &profile_name).await + .expect("Failed to create steel decimal test tables") +} + +// ======================================================================== +// DEBUGGING TESTS TO UNDERSTAND STEEL DECIMAL INTEGRATION +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_basic_function_registration(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test the most basic steel-decimal function to see if it's registered + let debug_table = format!("basic_debug_{}", generate_unique_id()); + let debug_def = PostTableDefinitionRequest { + profile_name: context.profile_name.clone(), + table_name: debug_table.clone(), + columns: vec![ + TableColumnDefinition { name: "test_input".into(), field_type: "text".into() }, + TableColumnDefinition { name: "result".into(), field_type: "text".into() }, + ], + indexes: vec![], + links: vec![], + }; + post_table_definition(&context.pool, debug_def).await.unwrap(); + + // Get table and schema IDs + let debug_table_id = sqlx::query_scalar!( + "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", + context.profile_name, + debug_table + ) + .fetch_one(&context.pool) + .await + .unwrap(); + + let schema_id = sqlx::query_scalar!( + "SELECT id FROM schemas WHERE name = $1", + context.profile_name + ) + .fetch_one(&context.pool) + .await + .unwrap(); + + // Test the simplest possible steel-decimal function: decimal-add + let simple_script = r#"(decimal-add "1.0" "2.0")"#; + + // Insert script + sqlx::query!( + "INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + debug_table_id, + debug_table, + "result", + "TEXT", + simple_script, + "Test basic decimal-add function", + schema_id + ) + .execute(&context.pool) + .await + .unwrap(); + + // Try to execute + let mut data = HashMap::new(); + data.insert("test_input".into(), proto_string("test")); + data.insert("result".into(), proto_string("3.0")); // Expected result of 1.0 + 2.0 + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: debug_table.clone(), + data, + }; + + let result = post_table_data(&context.pool, request, &indexer_tx).await; + match result { + Ok(response) => { + println!("✓ Basic steel-decimal function works!"); + + // Verify the result + let query = format!( + r#"SELECT result FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, debug_table + ); + let stored_result: String = sqlx::query_scalar(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + assert_eq!(stored_result, "3.0", "Steel-decimal function should return correct result"); + } + Err(e) => { + println!("✗ Basic steel-decimal function failed: {}", e.message()); + panic!("Steel-decimal functions are not properly registered"); + } + } +} + +#[rstest] +#[tokio::test] +async fn test_debug_steel_decimal_function_availability(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Try different possible function name formats to see what works + let test_scripts = vec![ + ("decimal-add", r#"(decimal-add "1.0" "2.0")"#), + ("decimal-mul", r#"(decimal-mul "2.0" "3.0")"#), + ("decimal-sub", r#"(decimal-sub "5.0" "2.0")"#), + ("decimal-div", r#"(decimal-div "10.0" "2.0")"#), + ("decimal-gt", r#"(decimal-gt "5.0" "3.0")"#), + ]; + + // Create a simple table for debugging + let debug_table = format!("debug_table_{}", generate_unique_id()); + let debug_def = PostTableDefinitionRequest { + profile_name: context.profile_name.clone(), + table_name: debug_table.clone(), + columns: vec![ + TableColumnDefinition { name: "test_name".into(), field_type: "text".into() }, + TableColumnDefinition { name: "result".into(), field_type: "text".into() }, + ], + indexes: vec![], + links: vec![], + }; + post_table_definition(&context.pool, debug_def).await.unwrap(); + + // Get table ID for script insertion + let debug_table_id = sqlx::query_scalar!( + "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", + context.profile_name, + debug_table + ) + .fetch_one(&context.pool) + .await + .unwrap(); + + let schema_id = sqlx::query_scalar!( + "SELECT id FROM schemas WHERE name = $1", + context.profile_name + ) + .fetch_one(&context.pool) + .await + .unwrap(); + + for (test_name, script) in test_scripts { + // Insert debug script + let insert_result = sqlx::query!( + "INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", + debug_table_id, + debug_table, + "result", + "TEXT", + script, + format!("Debug test for {}", test_name), + schema_id + ) + .execute(&context.pool) + .await; + + if insert_result.is_ok() { + // Try to execute the script + let mut data = HashMap::new(); + data.insert("test_name".into(), proto_string(test_name)); + data.insert("result".into(), proto_string("expected_value")); + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: debug_table.clone(), + data, + }; + + let result = post_table_data(&context.pool, request, &indexer_tx).await; + match result { + Ok(_) => println!("✓ Function {} works!", test_name), + Err(e) => println!("✗ Function {} failed: {}", test_name, e.message()), + } + + // Clean up the script for next test + sqlx::query!( + "DELETE FROM table_scripts WHERE table_definitions_id = $1 AND target_column = $2", + debug_table_id, + "result" + ) + .execute(&context.pool) + .await + .unwrap(); + } else { + println!("✗ Failed to insert debug script for {}", test_name); + } + } +} + +// ======================================================================== +// BASIC STEEL DECIMAL FUNCTIONALITY TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_basic_arithmetic(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test basic multiplication: quantity * unit_price = total_price + let mut data = HashMap::new(); + data.insert("item_name".into(), proto_string("Test Item")); + data.insert("quantity".into(), proto_number(5.0)); + data.insert("unit_price".into(), proto_string("19.99")); + data.insert("total_price".into(), proto_string("99.95")); // 5 * 19.99 = 99.95 + data.insert("tax_rate".into(), proto_string("0.1000")); // 10% + data.insert("final_amount".into(), proto_string("109.95")); // 99.95 * 1.1 = 109.945 -> 109.95 + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.invoice_table.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); + assert!(response.success); + + // Verify the calculated values were stored correctly + let query = format!( + r#"SELECT total_price, final_amount FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.invoice_table + ); + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_total: Decimal = row.get("total_price"); + let stored_final: Decimal = row.get("final_amount"); + + assert_eq!(stored_total, dec!(99.95)); + assert_eq!(stored_final, dec!(109.95)); +} + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_precision_preservation(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test high precision calculations + let mut data = HashMap::new(); + data.insert("operation_name".into(), proto_string("High Precision Test")); + data.insert("input_a".into(), proto_string("123.45678")); + data.insert("input_b".into(), proto_string("987.65432")); + // Steel script should calculate (a + b) * sqrt(a) and a/b * pi + data.insert("result".into(), proto_string("12345.67890")); // Placeholder, will be calculated + data.insert("precision_test".into(), proto_string("0.3926990125")); // Placeholder, will be calculated + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.calculation_table.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); + assert!(response.success); + + // Verify precision is maintained in stored values + let query = format!( + r#"SELECT result, precision_test FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.calculation_table + ); + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_result: Decimal = row.get("result"); + let stored_precision: Decimal = row.get("precision_test"); + + // Verify that the results are calculated by Steel and have proper precision + assert!(stored_result.scale() <= 5); // Should fit in decimal(20,5) + assert!(stored_precision.scale() <= 10); // Should fit in decimal(30,10) + println!("Steel calculated result: {}", stored_result); + println!("Steel calculated precision test: {}", stored_precision); +} + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_business_logic_validation(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test business logic with acceptable margin + let mut data = HashMap::new(); + data.insert("product_code".into(), proto_string("PROD001")); + data.insert("price".into(), proto_string("100.00")); + data.insert("discount_percent".into(), proto_string("15.00")); // 15% discount + data.insert("discounted_price".into(), proto_string("85.00")); // 100 * (1 - 0.15) = 85 + data.insert("margin_check".into(), proto_string("ACCEPTABLE")); // 15% margin > 20% minimum -> should be "TOO_LOW" + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.validation_table.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); + assert!(response.success); + + // Verify business logic was applied correctly + let query = format!( + r#"SELECT discounted_price, margin_check FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.validation_table + ); + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_discounted: Decimal = row.get("discounted_price"); + let stored_margin_check: String = row.get("margin_check"); + + assert_eq!(stored_discounted, dec!(85.00)); + assert_eq!(stored_margin_check, "TOO_LOW"); // 15% margin is less than 20% minimum +} + +// ======================================================================== +// STEEL DECIMAL ERROR HANDLING TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_division_by_zero_handling(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test division by zero in Steel script + let mut data = HashMap::new(); + data.insert("operation_name".into(), proto_string("Division by Zero Test")); + data.insert("input_a".into(), proto_string("100.00")); + data.insert("input_b".into(), proto_string("0.00")); // Division by zero + data.insert("result".into(), proto_string("0.00")); + data.insert("precision_test".into(), proto_string("0.00")); + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.calculation_table.clone(), + data, + }; + + let result = post_table_data(&context.pool, request, &indexer_tx).await; + + // Should fail due to division by zero in Steel script + assert!(result.is_err()); + if let Err(err) = result { + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("Script execution failed")); + } +} + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_invalid_input_format(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test invalid decimal format in Steel script input + let mut data = HashMap::new(); + data.insert("item_name".into(), proto_string("Invalid Input Test")); + data.insert("quantity".into(), proto_number(3.0)); + data.insert("unit_price".into(), proto_string("not-a-number")); // Invalid decimal + data.insert("total_price".into(), proto_string("0.00")); + data.insert("tax_rate".into(), proto_string("0.1000")); + data.insert("final_amount".into(), proto_string("0.00")); + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.invoice_table.clone(), + data, + }; + + let result = post_table_data(&context.pool, request, &indexer_tx).await; + + // Should fail due to invalid decimal format + assert!(result.is_err()); + if let Err(err) = result { + assert_eq!(err.code(), tonic::Code::InvalidArgument); + assert!(err.message().contains("Script execution failed")); + } +} + +// ======================================================================== +// STEEL DECIMAL ROUNDTRIP AND CONVERSION TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_roundtrip_conversion(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test various decimal formats and precision levels + let test_cases = vec![ + ("0.01", 2.0, "Small decimal"), + ("999.99", 10.0, "Large decimal with cents"), + ("123.456789", 1.0, "High precision input"), + ("0.00001", 100000.0, "Very small decimal"), + ]; + + for (unit_price, quantity, description) in test_cases { + let mut data = HashMap::new(); + data.insert("item_name".into(), proto_string(description)); + data.insert("quantity".into(), proto_number(quantity)); + data.insert("unit_price".into(), proto_string(unit_price)); + data.insert("total_price".into(), proto_string("0.00")); // Will be calculated + data.insert("tax_rate".into(), proto_string("0.0500")); // 5% + data.insert("final_amount".into(), proto_string("0.00")); // Will be calculated + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.invoice_table.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &indexer_tx).await + .expect(&format!("Failed for test case: {}", description)); + + // Verify that Steel decimal calculations are consistent + let query = format!( + r#"SELECT unit_price, quantity, total_price, final_amount FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.invoice_table + ); + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_unit_price: Decimal = row.get("unit_price"); + let stored_quantity: i32 = row.get("quantity"); + let stored_total: Decimal = row.get("total_price"); + let stored_final: Decimal = row.get("final_amount"); + + // Verify Steel calculations match expected Rust calculations + let expected_unit_price = Decimal::from_str(unit_price).unwrap(); + let expected_total = expected_unit_price * Decimal::from(stored_quantity); + let expected_final = expected_total * dec!(1.05); // 5% tax + + assert_eq!(stored_unit_price, expected_unit_price); + assert_eq!(stored_total, expected_total); + assert_eq!(stored_final, expected_final); + + println!("✓ Roundtrip test passed for {}: {} * {} = {} -> {}", + description, unit_price, quantity, stored_total, stored_final); + } +} + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_negative_values(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test negative discount (surcharge) + let mut data = HashMap::new(); + data.insert("product_code".into(), proto_string("SURCHG001")); + data.insert("price".into(), proto_string("50.00")); + data.insert("discount_percent".into(), proto_string("-10.00")); // 10% surcharge + data.insert("discounted_price".into(), proto_string("55.00")); // 50 * (1 - (-0.1)) = 50 * 1.1 = 55 + data.insert("margin_check".into(), proto_string("ACCEPTABLE")); // Will be calculated + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.validation_table.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); + + // Verify negative percentage handling + let query = format!( + r#"SELECT discounted_price, margin_check FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.validation_table + ); + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_discounted: Decimal = row.get("discounted_price"); + let stored_margin_check: String = row.get("margin_check"); + + assert_eq!(stored_discounted, dec!(55.00)); + // With surcharge, margin should be acceptable (negative discount increases final price) + assert_eq!(stored_margin_check, "ACCEPTABLE"); +} + +// ======================================================================== +// CONCURRENT STEEL DECIMAL EXECUTION TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_concurrent_steel_decimal_calculations(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + + use futures::future::join_all; + + // Test concurrent Steel decimal calculations + let tasks: Vec<_> = (0..10).map(|i| { + let context = context.clone(); + async move { + let indexer_tx = create_test_indexer_channel().await; + let unit_price = format!("{}.99", 10 + i); + let quantity = i + 1; + + let mut data = HashMap::new(); + data.insert("item_name".into(), proto_string(&format!("Concurrent Item {}", i))); + data.insert("quantity".into(), proto_number(quantity as f64)); + data.insert("unit_price".into(), proto_string(&unit_price)); + data.insert("total_price".into(), proto_string("0.00")); + data.insert("tax_rate".into(), proto_string("0.2000")); // 20% + data.insert("final_amount".into(), proto_string("0.00")); + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.invoice_table.clone(), + data, + }; + + post_table_data(&context.pool, request, &indexer_tx).await + } + }).collect(); + + let results = join_all(tasks).await; + + // All Steel calculations should succeed concurrently + for (i, result) in results.into_iter().enumerate() { + assert!(result.is_ok(), "Concurrent Steel calculation {} should succeed", i); + } + + // Verify all calculations were performed correctly + let query = format!( + r#"SELECT COUNT(*) as count FROM "{}"."{}" WHERE item_name LIKE 'Concurrent Item%'"#, + context.profile_name, context.invoice_table + ); + let count: i64 = sqlx::query_scalar(&query) + .fetch_one(&context.pool) + .await + .unwrap(); + + assert_eq!(count, 10); +} + +// ======================================================================== +// STEEL DECIMAL CRATE FEATURE VERIFICATION TESTS +// ======================================================================== + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_crate_features_available(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test that Steel decimal crate functions are properly registered + // This tests sqrt, which should only be available if steel-decimal is properly integrated + let mut data = HashMap::new(); + data.insert("operation_name".into(), proto_string("Steel Decimal Features Test")); + data.insert("input_a".into(), proto_string("25.00")); // sqrt(25) = 5 + data.insert("input_b".into(), proto_string("4.00")); // sqrt(4) = 2 + data.insert("result".into(), proto_string("0.00")); // Should be (25 + 4) * 5 = 145 + data.insert("precision_test".into(), proto_string("0.00")); // Should be 25/4 * pi ≈ 19.6349 + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.calculation_table.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); + + // Verify that advanced steel-decimal functions (like sqrt) worked + let query = format!( + r#"SELECT result, precision_test FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.calculation_table + ); + let row = sqlx::query(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + let stored_result: Decimal = row.get("result"); + let stored_precision: Decimal = row.get("precision_test"); + + // If steel-decimal is properly integrated, sqrt function should work + // Result should be (25 + 4) * sqrt(25) = 29 * 5 = 145 + assert_eq!(stored_result, dec!(145.00000)); + + // Precision test: 25/4 * π ≈ 19.634954... + assert!(stored_precision > dec!(19.6) && stored_precision < dec!(19.7)); + + println!("✓ Steel decimal crate features verified:"); + println!(" - sqrt function available and working"); + println!(" - High precision arithmetic: {}", stored_precision); + println!(" - Complex calculations: {}", stored_result); +} + +#[rstest] +#[tokio::test] +async fn test_steel_decimal_vs_native_rust_consistency(#[future] steel_decimal_context: SteelDecimalTestContext) { + let context = steel_decimal_context.await; + let indexer_tx = create_test_indexer_channel().await; + + // Test that Steel decimal calculations match native Rust decimal calculations + let test_cases = vec![ + ("100.00", "0.15", "Steel vs Rust consistency test 1"), + ("999.99", "0.0825", "Steel vs Rust consistency test 2"), + ("0.01", "0.9999", "Steel vs Rust edge case"), + ]; + + for (price, discount_rate, description) in test_cases { + let mut data = HashMap::new(); + data.insert("product_code".into(), proto_string("CONSISTENCY")); + data.insert("price".into(), proto_string(price)); + data.insert("discount_percent".into(), proto_string(&format!("{}", Decimal::from_str(discount_rate).unwrap() * dec!(100)))); + data.insert("discounted_price".into(), proto_string("0.00")); // Calculated by Steel + data.insert("margin_check".into(), proto_string("TEST")); // Calculated by Steel + + let request = PostTableDataRequest { + profile_name: context.profile_name.clone(), + table_name: context.validation_table.clone(), + data, + }; + + let response = post_table_data(&context.pool, request, &indexer_tx).await + .expect(&format!("Failed for consistency test: {}", description)); + + // Get Steel-calculated result + let query = format!( + r#"SELECT discounted_price FROM "{}"."{}" WHERE id = $1"#, + context.profile_name, context.validation_table + ); + let steel_result: Decimal = sqlx::query_scalar(&query) + .bind(response.inserted_id) + .fetch_one(&context.pool) + .await + .unwrap(); + + // Calculate using native Rust decimal + let price_decimal = Decimal::from_str(price).unwrap(); + let discount_decimal = Decimal::from_str(discount_rate).unwrap(); + let rust_result = price_decimal * (dec!(1) - discount_decimal); + + // Steel and Rust calculations should match exactly + assert_eq!(steel_result, rust_result, + "Steel vs Rust mismatch for {}: Steel={}, Rust={}", + description, steel_result, rust_result); + + println!("✓ Consistency verified for {}: {}", description, steel_result); + } +}