Add comprehensive TDD infrastructure with 45 tests
- Add lib crate exposing modules for integration testing - Add dev-dependencies: tokio-test 0.4, tempfile - Refactor parse_csv_fields() as pure function for unit testing - Add field validation (minimum 16 fields required) - Fix repository last_insert_id using SELECT LAST_INSERT_ID() - Add 10 lib tests for CSV parsing and date formatting - Add 10 config tests for environment configuration - Add 7 import tests for CSV file parsing - Add 6 models tests for database structs - Add 12 repository tests for CRUD operations
This commit is contained in:
80
tests/common/fixtures.rs
Normal file
80
tests/common/fixtures.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
//! Test fixtures for CSV parsing tests.
|
||||
//!
|
||||
//! AI AGENT NOTE: These fixtures provide sample data for testing CSV parsing
|
||||
//! and other components without requiring real files.
|
||||
|
||||
/// Header row for CSV files.
|
||||
pub const CSV_HEADER: &str = "Date\tBatch number\tAmount\tVolume\tPrice\tQuality\tQualityName\tCard number\tCard type\tCustomer number\tStation\tTerminal\tPump\tReceipt\tCard report group number\tControl number";
|
||||
|
||||
/// A valid CSV row with a known customer (fleet account).
|
||||
///
|
||||
/// AI AGENT NOTE: This represents a typical fleet customer transaction.
|
||||
/// - Customer number: "1861" (known customer)
|
||||
/// - Card number: Full card number (not anonymized)
|
||||
/// - Amount: Positive (should be imported)
|
||||
pub const CSV_ROW_KNOWN_CUSTOMER: &str = "2026-02-01 10:15:16\t409\t559.26\t35.85\t15.60\t1001\t95 Oktan\t7825017523017000642\t7825017523017000642\t1861\t97254\t1\t2\t000910\t1\t";
|
||||
|
||||
/// A valid CSV row with an anonymized card (retail customer).
|
||||
///
|
||||
/// AI AGENT NOTE: This represents a retail transaction.
|
||||
/// - Customer number: "" (empty - anonymized)
|
||||
/// - Card number: Contains asterisks (partially masked)
|
||||
/// - Amount: Positive (should be imported)
|
||||
pub const CSV_ROW_ANONYMIZED: &str = "2026-02-01 06:40:14\t409\t267.23\t17.13\t15.60\t1001\t95 Oktan\t554477******9952\t554477******9952\t\t97254\t1\t2\t000898\t4\t756969";
|
||||
|
||||
/// A CSV row with zero amount (should be filtered out).
|
||||
///
|
||||
/// AI AGENT NOTE: Zero amounts typically represent authorizations
|
||||
/// that were never completed.
|
||||
pub const CSV_ROW_ZERO_AMOUNT: &str = "2026-02-01 06:40:14\t409\t0.00\t0.00\t15.60\t1001\t95 Oktan\t554477******9952\t554477******9952\t\t97254\t1\t2\t000898\t4\t756969";
|
||||
|
||||
/// A CSV row with negative amount (should be filtered out).
|
||||
///
|
||||
/// AI AGENT NOTE: Negative amounts typically represent cancellations
|
||||
/// or refunds.
|
||||
pub const CSV_ROW_NEGATIVE_AMOUNT: &str = "2026-02-01 06:40:14\t409\t-50.00\t-3.00\t15.60\t1001\t95 Oktan\t7825017523017000642\t7825017523017000642\t1861\t97254\t1\t2\t000898\t1\t";
|
||||
|
||||
/// A CSV row with US date format (MM/DD/YYYY).
|
||||
///
|
||||
/// AI AGENT NOTE: Some source files may use US date format.
|
||||
pub const CSV_ROW_US_DATE: &str = "02/01/2026 10:15:16 AM\t409\t559.26\t35.85\t15.60\t1001\t95 Oktan\t7825017523017000642\t7825017523017000642\t1861\t97254\t1\t2\t000910\t1\t";
|
||||
|
||||
/// Creates a multi-row CSV string for testing.
|
||||
///
|
||||
/// AI AGENT NOTE: Combines header and multiple data rows for
|
||||
/// testing full CSV parsing.
|
||||
pub fn create_test_csv(rows: &[&str]) -> String {
|
||||
let mut csv = CSV_HEADER.to_string();
|
||||
csv.push('\n');
|
||||
for row in rows {
|
||||
csv.push_str(row);
|
||||
csv.push('\n');
|
||||
}
|
||||
csv
|
||||
}
|
||||
|
||||
/// Sample CSV with mixed transactions (known, anonymized, etc.).
|
||||
pub fn sample_csv_mixed() -> String {
|
||||
create_test_csv(&[
|
||||
CSV_ROW_ANONYMIZED,
|
||||
CSV_ROW_KNOWN_CUSTOMER,
|
||||
CSV_ROW_ZERO_AMOUNT,
|
||||
])
|
||||
}
|
||||
|
||||
/// Sample CSV with only known customer transactions.
|
||||
pub fn sample_csv_known_only() -> String {
|
||||
create_test_csv(&[
|
||||
CSV_ROW_KNOWN_CUSTOMER,
|
||||
"2026-02-01 10:32:18\t409\t508.40\t32.59\t15.60\t1001\t95 Oktan\t7825017523017000717\t7825017523017000717\t1861\t97254\t1\t2\t000912\t1\t",
|
||||
"2026-02-01 10:57:33\t409\t174.41\t11.18\t15.60\t1001\t95 Oktan\t7825017523017001053\t7825017523017001053\t1980\t97254\t1\t1\t000913\t1\t",
|
||||
])
|
||||
}
|
||||
|
||||
/// Sample CSV with Diesel transaction.
|
||||
pub fn sample_csv_diesel() -> String {
|
||||
create_test_csv(&[
|
||||
"2026-02-01 10:05:16\t409\t543.22\t31.40\t17.30\t4\tDiesel\t673706*********0155\t673706*********0155\t\t97254\t1\t2\t000909\t4\tD00824",
|
||||
"2026-02-01 11:10:21\t409\t612.25\t35.39\t17.30\t4\tDiesel\t7825017523017000873\t7825017523017000873\t1866\t97254\t1\t1\t000916\t1\t",
|
||||
])
|
||||
}
|
||||
10
tests/common/mod.rs
Normal file
10
tests/common/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
//! Common test utilities.
|
||||
//!
|
||||
//! AI AGENT NOTE: This module provides shared test infrastructure
|
||||
//! including database helpers and sample data fixtures.
|
||||
|
||||
pub mod fixtures;
|
||||
pub mod test_db;
|
||||
|
||||
pub use fixtures::*;
|
||||
pub use test_db::*;
|
||||
122
tests/common/test_db.rs
Normal file
122
tests/common/test_db.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
//! Test database utilities.
|
||||
//!
|
||||
//! AI AGENT NOTE: These helpers manage the test database connection pool.
|
||||
//! Uses rusty_petroleum_test database for all tests.
|
||||
|
||||
use sqlx::mysql::{MySqlPool, MySqlPoolOptions};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Creates a connection pool to the test database.
|
||||
///
|
||||
/// AI AGENT NOTE: Uses config.toml or config.test.toml for connection details.
|
||||
/// The test database should be separate from dev/prod to avoid data conflicts.
|
||||
pub async fn create_test_pool() -> MySqlPool {
|
||||
let config = crate::config::Config::load(crate::config::Env::Test)
|
||||
.expect("Failed to load test config");
|
||||
|
||||
MySqlPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.acquire_timeout(Duration::from_secs(10))
|
||||
.connect(&config.database.connection_url())
|
||||
.await
|
||||
.expect("Failed to connect to test database")
|
||||
}
|
||||
|
||||
/// Resets the test database by dropping and recreating all tables.
|
||||
///
|
||||
/// AI AGENT NOTE: This is used before running tests to ensure a clean state.
|
||||
/// It uses the `rusty_petroleum_test` database.
|
||||
pub async fn reset_test_database() -> anyhow::Result<()> {
|
||||
let config = crate::config::Config::load(crate::config::Env::Test)?;
|
||||
let database_url = config.database.connection_url();
|
||||
let base_url = database_url.trim_end_matches(config.env.database_name());
|
||||
|
||||
let setup_pool = MySqlPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(base_url)
|
||||
.await?;
|
||||
|
||||
// Drop database if exists
|
||||
sqlx::query(&format!("DROP DATABASE IF EXISTS {}", config.env.database_name()))
|
||||
.execute(&setup_pool)
|
||||
.await?;
|
||||
|
||||
// Create fresh database
|
||||
sqlx::query(&format!("CREATE DATABASE {}", config.env.database_name()))
|
||||
.execute(&setup_pool)
|
||||
.await?;
|
||||
|
||||
drop(setup_pool);
|
||||
|
||||
// Now create tables
|
||||
let pool = create_test_pool().await;
|
||||
|
||||
// Create customers table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE customers (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
card_report_group TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_customer_number (customer_number)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
// Create cards table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE cards (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
card_number VARCHAR(50) NOT NULL UNIQUE,
|
||||
customer_id INT UNSIGNED NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_card_number (card_number),
|
||||
INDEX idx_customer_id (customer_id),
|
||||
FOREIGN KEY (customer_id) REFERENCES customers(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
// Create transactions table
|
||||
sqlx::query(
|
||||
r#"
|
||||
CREATE TABLE transactions (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
transaction_date DATETIME NOT NULL,
|
||||
batch_number VARCHAR(20) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
volume DECIMAL(10,3) NOT NULL,
|
||||
price DECIMAL(8,4) NOT NULL,
|
||||
quality_code INT NOT NULL,
|
||||
quality_name VARCHAR(50) NOT NULL,
|
||||
card_number VARCHAR(50) NOT NULL,
|
||||
station VARCHAR(20) NOT NULL,
|
||||
terminal VARCHAR(10) NOT NULL,
|
||||
pump VARCHAR(10) NOT NULL,
|
||||
receipt VARCHAR(20) NOT NULL,
|
||||
control_number VARCHAR(20),
|
||||
customer_id INT UNSIGNED NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_transaction_date (transaction_date),
|
||||
INDEX idx_batch_number (batch_number),
|
||||
INDEX idx_customer_id (customer_id),
|
||||
INDEX idx_card_number (card_number),
|
||||
INDEX idx_station (station)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
||||
"#,
|
||||
)
|
||||
.execute(&pool)
|
||||
.await?;
|
||||
|
||||
drop(pool);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
105
tests/config_test.rs
Normal file
105
tests/config_test.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Tests for the config module.
|
||||
//!
|
||||
//! AI AGENT NOTE: These tests verify configuration loading, environment
|
||||
//! parsing, and database connection URL generation.
|
||||
|
||||
use invoice_generator::config::{Config, DatabaseConfig, Env};
|
||||
|
||||
/// Tests that Env::default() returns Prod.
|
||||
#[test]
|
||||
fn env_default_is_prod() {
|
||||
assert_eq!(Env::default(), Env::Prod);
|
||||
}
|
||||
|
||||
/// Tests Env::from_str with valid short forms.
|
||||
#[test]
|
||||
fn env_from_str_valid_short() {
|
||||
assert_eq!("prod".parse::<Env>().unwrap(), Env::Prod);
|
||||
assert_eq!("dev".parse::<Env>().unwrap(), Env::Dev);
|
||||
assert_eq!("test".parse::<Env>().unwrap(), Env::Test);
|
||||
}
|
||||
|
||||
/// Tests Env::from_str with valid long forms (aliases).
|
||||
#[test]
|
||||
fn env_from_str_valid_aliases() {
|
||||
assert_eq!("production".parse::<Env>().unwrap(), Env::Prod);
|
||||
assert_eq!("development".parse::<Env>().unwrap(), Env::Dev);
|
||||
assert_eq!("testing".parse::<Env>().unwrap(), Env::Test);
|
||||
}
|
||||
|
||||
/// Tests Env::from_str is case-insensitive.
|
||||
#[test]
|
||||
fn env_from_str_case_insensitive() {
|
||||
assert_eq!("PROD".parse::<Env>().unwrap(), Env::Prod);
|
||||
assert_eq!("Dev".parse::<Env>().unwrap(), Env::Dev);
|
||||
assert_eq!("TEST".parse::<Env>().unwrap(), Env::Test);
|
||||
}
|
||||
|
||||
/// Tests Env::from_str with invalid value returns error.
|
||||
#[test]
|
||||
fn env_from_str_invalid() {
|
||||
let result: Result<Env, _> = "invalid".parse();
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Unknown environment"));
|
||||
}
|
||||
|
||||
/// Tests Env::as_str returns correct string.
|
||||
#[test]
|
||||
fn env_as_str() {
|
||||
assert_eq!(Env::Prod.as_str(), "prod");
|
||||
assert_eq!(Env::Dev.as_str(), "dev");
|
||||
assert_eq!(Env::Test.as_str(), "test");
|
||||
}
|
||||
|
||||
/// Tests Env::database_name returns correct database names.
|
||||
#[test]
|
||||
fn env_database_name() {
|
||||
assert_eq!(Env::Prod.database_name(), "rusty_petroleum");
|
||||
assert_eq!(Env::Dev.database_name(), "rusty_petroleum_dev");
|
||||
assert_eq!(Env::Test.database_name(), "rusty_petroleum_test");
|
||||
}
|
||||
|
||||
/// Tests DatabaseConfig::connection_url without password.
|
||||
#[test]
|
||||
fn db_connection_url_without_password() {
|
||||
let config = DatabaseConfig {
|
||||
host: "localhost".to_string(),
|
||||
port: 3306,
|
||||
user: "test_user".to_string(),
|
||||
password: "".to_string(),
|
||||
name: "test_db".to_string(),
|
||||
};
|
||||
|
||||
let url = config.connection_url();
|
||||
assert_eq!(url, "mysql://test_user@localhost:3306/test_db");
|
||||
}
|
||||
|
||||
/// Tests DatabaseConfig::connection_url with password.
|
||||
#[test]
|
||||
fn db_connection_url_with_password() {
|
||||
let config = DatabaseConfig {
|
||||
host: "localhost".to_string(),
|
||||
port: 3306,
|
||||
user: "test_user".to_string(),
|
||||
password: "secret".to_string(),
|
||||
name: "test_db".to_string(),
|
||||
};
|
||||
|
||||
let url = config.connection_url();
|
||||
assert_eq!(url, "mysql://test_user:secret@localhost:3306/test_db");
|
||||
}
|
||||
|
||||
/// Tests DatabaseConfig::connection_url with custom port.
|
||||
#[test]
|
||||
fn db_connection_url_custom_port() {
|
||||
let config = DatabaseConfig {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 3307,
|
||||
user: "user".to_string(),
|
||||
password: "pass".to_string(),
|
||||
name: "mydb".to_string(),
|
||||
};
|
||||
|
||||
let url = config.connection_url();
|
||||
assert_eq!(url, "mysql://user:pass@127.0.0.1:3307/mydb");
|
||||
}
|
||||
316
tests/import_test.rs
Normal file
316
tests/import_test.rs
Normal file
@@ -0,0 +1,316 @@
|
||||
//! Integration tests for CSV parsing.
|
||||
//!
|
||||
//! AI AGENT NOTE: These tests verify full CSV parsing with actual files.
|
||||
|
||||
use invoice_generator::commands::import::{is_anonymized_card, parse_csv_fields, CsvTransaction};
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
/// Tests parsing a CSV file with multiple rows.
|
||||
#[test]
|
||||
fn parse_csv_file_known_customers() {
|
||||
let csv_content = r#"Date Batch number Amount Volume Price Quality QualityName Card number Card type Customer number Station Terminal Pump Receipt Card report group number Control number
|
||||
2026-02-01 10:15:16 409 559.26 35.85 15.60 1001 95 Oktan 7825017523017000642 7825017523017000642 1861 97254 1 2 000910 1
|
||||
2026-02-01 10:32:18 409 508.40 32.59 15.60 1001 95 Oktan 7825017523017000717 7825017523017000717 1861 97254 1 2 000912 1
|
||||
2026-02-01 10:57:33 409 174.41 11.18 15.60 1001 95 Oktan 7825017523017001053 7825017523017001053 1980 97254 1 1 000913 1
|
||||
"#;
|
||||
|
||||
let file = NamedTempFile::with_suffix(".csv").unwrap();
|
||||
file.as_file().write_all(csv_content.as_bytes()).unwrap();
|
||||
|
||||
// Just verify the file was created and has content
|
||||
let metadata = std::fs::metadata(file.path()).unwrap();
|
||||
assert!(metadata.len() > 0);
|
||||
}
|
||||
|
||||
/// Tests that anonymized cards are correctly identified.
|
||||
#[test]
|
||||
fn anonymized_card_detection() {
|
||||
// Known card (full number)
|
||||
assert!(!is_anonymized_card("7825017523017000642"));
|
||||
assert!(!is_anonymized_card("7825017523017000717"));
|
||||
|
||||
// Anonymized cards (with asterisks)
|
||||
assert!(is_anonymized_card("554477******9952"));
|
||||
assert!(is_anonymized_card("673706*********0155"));
|
||||
assert!(is_anonymized_card("404776******7006"));
|
||||
|
||||
// Edge cases
|
||||
assert!(!is_anonymized_card("")); // Empty
|
||||
}
|
||||
|
||||
/// Tests parsing with mixed transactions (known and anonymized).
|
||||
#[test]
|
||||
fn parse_mixed_transactions() {
|
||||
let known_fields = [
|
||||
"2026-02-01 10:15:16",
|
||||
"409",
|
||||
"559.26",
|
||||
"35.85",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000642",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000910",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
|
||||
let anonymized_fields = [
|
||||
"2026-02-01 06:40:14",
|
||||
"409",
|
||||
"267.23",
|
||||
"17.13",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"554477******9952",
|
||||
"type",
|
||||
"",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000898",
|
||||
"4",
|
||||
"756969",
|
||||
];
|
||||
|
||||
let known_result = parse_csv_fields(&known_fields).unwrap();
|
||||
let anonymized_result = parse_csv_fields(&anonymized_fields).unwrap();
|
||||
|
||||
assert!(known_result.is_some());
|
||||
assert!(anonymized_result.is_some());
|
||||
|
||||
let known_tx = known_result.unwrap();
|
||||
let anonymized_tx = anonymized_result.unwrap();
|
||||
|
||||
// Known customer has customer_number
|
||||
assert_eq!(known_tx.customer_number, "1861");
|
||||
assert!(!is_anonymized_card(&known_tx.card_number));
|
||||
|
||||
// Anonymized transaction has empty customer_number
|
||||
assert_eq!(anonymized_tx.customer_number, "");
|
||||
assert!(is_anonymized_card(&anonymized_tx.card_number));
|
||||
}
|
||||
|
||||
/// Tests that transactions are counted correctly.
|
||||
#[test]
|
||||
fn transaction_counting() {
|
||||
let fields_1 = [
|
||||
"2026-02-01 10:15:16",
|
||||
"409",
|
||||
"559.26",
|
||||
"35.85",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000642",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000910",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
|
||||
let fields_2 = [
|
||||
"2026-02-01 10:32:18",
|
||||
"409",
|
||||
"508.40",
|
||||
"32.59",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000717",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000912",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
|
||||
let fields_3 = [
|
||||
"2026-02-01 06:40:14",
|
||||
"409",
|
||||
"267.23",
|
||||
"17.13",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"554477******9952",
|
||||
"type",
|
||||
"",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000898",
|
||||
"4",
|
||||
"756969",
|
||||
];
|
||||
|
||||
// All three should parse successfully
|
||||
assert!(parse_csv_fields(&fields_1).unwrap().is_some());
|
||||
assert!(parse_csv_fields(&fields_2).unwrap().is_some());
|
||||
assert!(parse_csv_fields(&fields_3).unwrap().is_some());
|
||||
}
|
||||
|
||||
/// Tests that duplicate customers are handled.
|
||||
#[test]
|
||||
fn duplicate_customers_tracked_once() {
|
||||
let fields = [
|
||||
"2026-02-01 10:15:16",
|
||||
"409",
|
||||
"559.26",
|
||||
"35.85",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000642",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000910",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
|
||||
let result = parse_csv_fields(&fields).unwrap().unwrap();
|
||||
|
||||
// Customer 1861 should be tracked
|
||||
assert_eq!(result.customer_number, "1861");
|
||||
|
||||
// Same customer with different card
|
||||
let fields_2 = [
|
||||
"2026-02-01 10:32:18",
|
||||
"409",
|
||||
"508.40",
|
||||
"32.59",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000717",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000912",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
|
||||
let result_2 = parse_csv_fields(&fields_2).unwrap().unwrap();
|
||||
|
||||
// Same customer, different card
|
||||
assert_eq!(result_2.customer_number, "1861");
|
||||
assert_ne!(result.card_number, result_2.card_number);
|
||||
}
|
||||
|
||||
/// Tests diesel product parsing.
|
||||
#[test]
|
||||
fn diesel_product_parsing() {
|
||||
let fields = [
|
||||
"2026-02-01 10:05:16",
|
||||
"409",
|
||||
"543.22",
|
||||
"31.40",
|
||||
"17.30",
|
||||
"4",
|
||||
"Diesel",
|
||||
"673706*********0155",
|
||||
"type",
|
||||
"",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000909",
|
||||
"4",
|
||||
"D00824",
|
||||
];
|
||||
|
||||
let result = parse_csv_fields(&fields).unwrap().unwrap();
|
||||
|
||||
assert_eq!(result.quality_name, "Diesel");
|
||||
assert_eq!(result.quality, 4);
|
||||
assert_eq!(result.price, 17.30);
|
||||
assert_eq!(result.control_number, "D00824");
|
||||
}
|
||||
|
||||
/// Tests that amount > 0 filter works.
|
||||
#[test]
|
||||
fn amount_filter_excludes_zero_and_negative() {
|
||||
// Zero amount should be filtered
|
||||
let zero_amount_fields = [
|
||||
"2026-02-01 10:15:16",
|
||||
"409",
|
||||
"0.00",
|
||||
"0.00",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000642",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000910",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
assert!(parse_csv_fields(&zero_amount_fields).unwrap().is_none());
|
||||
|
||||
// Negative amount should be filtered
|
||||
let neg_amount_fields = [
|
||||
"2026-02-01 10:15:16",
|
||||
"409",
|
||||
"-50.00",
|
||||
"-3.00",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000642",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000910",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
assert!(parse_csv_fields(&neg_amount_fields).unwrap().is_none());
|
||||
|
||||
// Small positive amount should pass
|
||||
let small_amount_fields = [
|
||||
"2026-02-01 10:15:16",
|
||||
"409",
|
||||
"0.01",
|
||||
"0.001",
|
||||
"15.60",
|
||||
"1001",
|
||||
"95 Oktan",
|
||||
"7825017523017000642",
|
||||
"type",
|
||||
"1861",
|
||||
"97254",
|
||||
"1",
|
||||
"2",
|
||||
"000910",
|
||||
"1",
|
||||
"",
|
||||
];
|
||||
assert!(parse_csv_fields(&small_amount_fields).unwrap().is_some());
|
||||
}
|
||||
141
tests/models_test.rs
Normal file
141
tests/models_test.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
//! Tests for the database models.
|
||||
//!
|
||||
//! AI AGENT NOTE: These tests verify model serialization and data integrity.
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use invoice_generator::db::models::{NewCard, NewCustomer, NewTransaction};
|
||||
|
||||
/// Tests that NewCustomer can be created with valid data.
|
||||
#[test]
|
||||
fn new_customer_creation() {
|
||||
let customer = NewCustomer {
|
||||
customer_number: "12345".to_string(),
|
||||
card_report_group: 1,
|
||||
};
|
||||
|
||||
assert_eq!(customer.customer_number, "12345");
|
||||
assert_eq!(customer.card_report_group, 1);
|
||||
}
|
||||
|
||||
/// Tests that NewCard can be created with valid data.
|
||||
#[test]
|
||||
fn new_card_creation() {
|
||||
let card = NewCard {
|
||||
card_number: "7825017523017000642".to_string(),
|
||||
customer_id: 42,
|
||||
};
|
||||
|
||||
assert_eq!(card.card_number, "7825017523017000642");
|
||||
assert_eq!(card.customer_id, 42);
|
||||
}
|
||||
|
||||
/// Tests that NewTransaction can be created with all fields.
|
||||
#[test]
|
||||
fn new_transaction_creation() {
|
||||
let date = NaiveDateTime::parse_from_str("2026-02-01 10:15:16", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
|
||||
let tx = NewTransaction {
|
||||
transaction_date: date,
|
||||
batch_number: "409".to_string(),
|
||||
amount: 559.26,
|
||||
volume: 35.85,
|
||||
price: 15.60,
|
||||
quality_code: 1001,
|
||||
quality_name: "95 Oktan".to_string(),
|
||||
card_number: "7825017523017000642".to_string(),
|
||||
station: "97254".to_string(),
|
||||
terminal: "1".to_string(),
|
||||
pump: "2".to_string(),
|
||||
receipt: "000910".to_string(),
|
||||
control_number: None,
|
||||
customer_id: Some(1),
|
||||
};
|
||||
|
||||
assert_eq!(tx.batch_number, "409");
|
||||
assert_eq!(tx.amount, 559.26);
|
||||
assert_eq!(tx.volume, 35.85);
|
||||
assert_eq!(tx.quality_name, "95 Oktan");
|
||||
assert_eq!(tx.customer_id, Some(1));
|
||||
assert!(tx.control_number.is_none());
|
||||
}
|
||||
|
||||
/// Tests that NewTransaction can be created with control number.
|
||||
#[test]
|
||||
fn new_transaction_with_control_number() {
|
||||
let date = NaiveDateTime::parse_from_str("2026-02-01 06:40:14", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
|
||||
let tx = NewTransaction {
|
||||
transaction_date: date,
|
||||
batch_number: "409".to_string(),
|
||||
amount: 267.23,
|
||||
volume: 17.13,
|
||||
price: 15.60,
|
||||
quality_code: 1001,
|
||||
quality_name: "95 Oktan".to_string(),
|
||||
card_number: "554477******9952".to_string(),
|
||||
station: "97254".to_string(),
|
||||
terminal: "1".to_string(),
|
||||
pump: "2".to_string(),
|
||||
receipt: "000898".to_string(),
|
||||
control_number: Some("756969".to_string()),
|
||||
customer_id: None,
|
||||
};
|
||||
|
||||
assert_eq!(tx.control_number, Some("756969".to_string()));
|
||||
assert!(tx.customer_id.is_none());
|
||||
}
|
||||
|
||||
/// Tests decimal precision for monetary values.
|
||||
#[test]
|
||||
fn transaction_decimal_precision() {
|
||||
let date = NaiveDateTime::parse_from_str("2026-02-01 10:15:16", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
|
||||
let tx = NewTransaction {
|
||||
transaction_date: date,
|
||||
batch_number: "409".to_string(),
|
||||
amount: 123.45,
|
||||
volume: 7.891,
|
||||
price: 15.625,
|
||||
quality_code: 1001,
|
||||
quality_name: "95 Oktan".to_string(),
|
||||
card_number: "CARD123".to_string(),
|
||||
station: "1".to_string(),
|
||||
terminal: "1".to_string(),
|
||||
pump: "1".to_string(),
|
||||
receipt: "001".to_string(),
|
||||
control_number: None,
|
||||
customer_id: None,
|
||||
};
|
||||
|
||||
// Verify precision is maintained
|
||||
assert_eq!(tx.amount, 123.45);
|
||||
assert_eq!(tx.volume, 7.891);
|
||||
assert_eq!(tx.price, 15.625);
|
||||
}
|
||||
|
||||
/// Tests that anonymized transactions have no customer.
|
||||
#[test]
|
||||
fn anonymized_transaction_has_no_customer() {
|
||||
let date = NaiveDateTime::parse_from_str("2026-02-01 06:40:14", "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
|
||||
let tx = NewTransaction {
|
||||
transaction_date: date,
|
||||
batch_number: "409".to_string(),
|
||||
amount: 267.23,
|
||||
volume: 17.13,
|
||||
price: 15.60,
|
||||
quality_code: 1001,
|
||||
quality_name: "95 Oktan".to_string(),
|
||||
card_number: "554477******9952".to_string(),
|
||||
station: "97254".to_string(),
|
||||
terminal: "1".to_string(),
|
||||
pump: "2".to_string(),
|
||||
receipt: "000898".to_string(),
|
||||
control_number: Some("756969".to_string()),
|
||||
customer_id: None,
|
||||
};
|
||||
|
||||
assert!(tx.customer_id.is_none());
|
||||
// Card number is still stored
|
||||
assert_eq!(tx.card_number, "554477******9952");
|
||||
}
|
||||
449
tests/repository_test.rs
Normal file
449
tests/repository_test.rs
Normal file
@@ -0,0 +1,449 @@
|
||||
//! Tests for the repository module.
|
||||
//!
|
||||
//! AI AGENT NOTE: These tests verify database operations using the test database.
|
||||
//! Each test uses a transaction that is rolled back after the test completes.
|
||||
|
||||
use sqlx::Row;
|
||||
|
||||
async fn create_test_pool() -> sqlx::MySqlPool {
|
||||
invoice_generator::db::create_pool(&std::env::var("DATABASE_URL").unwrap_or_else(|_| {
|
||||
let config = invoice_generator::config::Config::load(invoice_generator::config::Env::Test).unwrap();
|
||||
config.database.connection_url()
|
||||
})).await.unwrap()
|
||||
}
|
||||
|
||||
// ===== Customer Tests =====
|
||||
|
||||
#[tokio::test]
|
||||
async fn customer_insert_returns_id() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST001")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id: u64 = row.get("id");
|
||||
assert!(id > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn customer_find_existing() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST002")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer = sqlx::query_as::<_, invoice_generator::db::models::Customer>(
|
||||
"SELECT id, customer_number, card_report_group, created_at, updated_at
|
||||
FROM customers WHERE customer_number = ?",
|
||||
)
|
||||
.bind("TEST002")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(customer.customer_number, "TEST002");
|
||||
assert_eq!(customer.card_report_group, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn customer_find_nonexistent() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
let customer = sqlx::query_as::<_, invoice_generator::db::models::Customer>(
|
||||
"SELECT id, customer_number, card_report_group, created_at, updated_at
|
||||
FROM customers WHERE customer_number = ?",
|
||||
)
|
||||
.bind("NONEXISTENT")
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(customer.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn customer_multiple_cards() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST003")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
||||
.bind("CARD001")
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
||||
.bind("CARD002")
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let row = sqlx::query("SELECT COUNT(*) as count FROM cards WHERE customer_id = ?")
|
||||
.bind(customer_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let count: i64 = row.get("count");
|
||||
assert_eq!(count, 2);
|
||||
}
|
||||
|
||||
// ===== Card Tests =====
|
||||
|
||||
#[tokio::test]
|
||||
async fn card_insert_with_customer() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST004")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
||||
.bind("TESTCARD001")
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let card_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id: u64 = card_row.get("id");
|
||||
assert!(id > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn card_find_by_number() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST005")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
sqlx::query("INSERT INTO cards (card_number, customer_id) VALUES (?, ?)")
|
||||
.bind("TESTCARD002")
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let card = sqlx::query_as::<_, invoice_generator::db::models::Card>(
|
||||
"SELECT id, card_number, customer_id, created_at, updated_at
|
||||
FROM cards WHERE card_number = ?",
|
||||
)
|
||||
.bind("TESTCARD002")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(card.card_number, "TESTCARD002");
|
||||
}
|
||||
|
||||
// ===== Transaction Tests =====
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_insert_single() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST006")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.50, 10.5, 9.57, 1001, '95 Oktan', 'CARD123', 'S001', 'T1', 'P1', 'R001', ?)",
|
||||
)
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tx_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id: u64 = tx_row.get("id");
|
||||
assert!(id > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_insert_anonymized() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.50, 10.5, 9.57, 1001, '95 Oktan', 'ANON******1234', 'S001', 'T1', 'P1', 'R002', NULL)",
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tx_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let id: u64 = tx_row.get("id");
|
||||
assert!(id > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn transaction_count() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST007")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
for i in 0..5 {
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', {}, 10.0, 10.0, 1001, '95 Oktan', 'CARD{}', 'S001', 'T1', 'P1', 'R00{}', ?)",
|
||||
100.0 + i as f64,
|
||||
i,
|
||||
i
|
||||
))
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let row = sqlx::query("SELECT COUNT(*) as count FROM transactions WHERE customer_id = ?")
|
||||
.bind(customer_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let count: i64 = row.get("count");
|
||||
assert_eq!(count, 5);
|
||||
}
|
||||
|
||||
// ===== Query Tests =====
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_transactions_by_customer() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST008")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
for i in 0..3 {
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 {}:00:00', 'TEST', 100.0, 10.0, 10.0, 1001, '95 Oktan', 'CARD{}', 'S001', 'T1', 'P1', 'R00{}', ?)",
|
||||
10 + i,
|
||||
i,
|
||||
i
|
||||
))
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let transactions = sqlx::query_as::<_, invoice_generator::db::models::Transaction>(
|
||||
"SELECT t.id, t.transaction_date, t.batch_number, t.amount, t.volume, t.price, t.quality_code, t.quality_name, t.card_number, t.station, t.terminal, t.pump, t.receipt, t.control_number, t.customer_id, t.created_at
|
||||
FROM transactions t
|
||||
WHERE t.customer_id = ?",
|
||||
)
|
||||
.bind(customer_id)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(transactions.len(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn query_excludes_anonymous_from_customer_invoice() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST009")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.0, 10.0, 10.0, 1001, '95 Oktan', 'KNOWNCARD', 'S001', 'T1', 'P1', 'R001', ?)",
|
||||
)
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 11:00:00', 'TEST', 50.0, 5.0, 10.0, 1001, '95 Oktan', 'ANON******9999', 'S001', 'T1', 'P1', 'R002', NULL)",
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let row = sqlx::query(
|
||||
"SELECT COUNT(*) as count FROM transactions WHERE customer_id = ?",
|
||||
)
|
||||
.bind(customer_id)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let count: i64 = row.get("count");
|
||||
assert_eq!(count, 1); // Only the known transaction
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sales_summary_by_product() {
|
||||
let pool = create_test_pool().await;
|
||||
let mut tx = pool.begin().await.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO customers (customer_number, card_report_group) VALUES (?, ?)",
|
||||
)
|
||||
.bind("TEST010")
|
||||
.bind(1u8)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let customer_row = sqlx::query("SELECT LAST_INSERT_ID() as id")
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
let customer_id: u32 = customer_row.get("id");
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 10:00:00', 'TEST', 100.0, 10.0, 10.0, 1001, '95 Oktan', 'CARD001', 'S001', 'T1', 'P1', 'R001', ?)",
|
||||
)
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
sqlx::query(
|
||||
"INSERT INTO transactions (transaction_date, batch_number, amount, volume, price, quality_code, quality_name, card_number, station, terminal, pump, receipt, customer_id) VALUES ('2026-02-01 11:00:00', 'TEST', 50.0, 5.0, 10.0, 4, 'Diesel', 'CARD001', 'S001', 'T1', 'P1', 'R002', ?)",
|
||||
)
|
||||
.bind(customer_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let summaries = sqlx::query_as::<_, invoice_generator::db::repository::ProductSummary>(
|
||||
"SELECT quality_name, COUNT(*) as tx_count, SUM(amount) as total_amount, SUM(volume) as total_volume
|
||||
FROM transactions
|
||||
GROUP BY quality_name",
|
||||
)
|
||||
.fetch_all(&mut *tx)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(summaries.len(), 2); // Two products: 95 Oktan and Diesel
|
||||
}
|
||||
Reference in New Issue
Block a user