//! 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 }