stateset-db 1.22.0

Database implementations for StateSet iCommerce
#![cfg(feature = "sqlite")]

use rust_decimal::Decimal;
use stateset_core::{
    AdjustInventory, CreateInventoryItem, CreateInvoice, CreateInvoiceItem, CreatePurchaseOrder,
    CreatePurchaseOrderItem, CreateShipment, CreateShipmentItem, CreateSupplier,
    InventoryRepository, InvoiceRepository, PurchaseOrderRepository, ReserveInventory,
    ShipmentRepository,
};
use stateset_db::SqliteDatabase;
use uuid::Uuid;

#[test]
fn sqlite_invoice_create_rolls_back_when_item_insert_fails() {
    let db = SqliteDatabase::in_memory().expect("create in-memory sqlite db");
    {
        let conn = db.conn().expect("get sqlite connection");
        conn.execute("DROP TABLE invoice_items", []).expect("drop invoice_items");
    }

    let repo = db.invoices();
    repo.create(CreateInvoice {
        customer_id: Uuid::new_v4().into(),
        items: vec![CreateInvoiceItem {
            description: "Test item".to_string(),
            quantity: Decimal::ONE,
            unit_price: Decimal::ONE,
            ..Default::default()
        }],
        ..Default::default()
    })
    .expect_err("expected create invoice to fail");

    let conn = db.conn().expect("get sqlite connection");
    let count: i64 = conn
        .query_row("SELECT COUNT(*) FROM invoices", [], |row| row.get(0))
        .expect("count invoices");
    assert_eq!(count, 0, "invoice insert should have been rolled back");
}

#[test]
fn sqlite_purchase_order_create_rolls_back_when_item_insert_fails() {
    let db = SqliteDatabase::in_memory().expect("create in-memory sqlite db");
    let repo = db.purchase_orders();

    let supplier = repo
        .create_supplier(CreateSupplier { name: "Test supplier".to_string(), ..Default::default() })
        .expect("create supplier");

    {
        let conn = db.conn().expect("get sqlite connection");
        conn.execute("DROP TABLE purchase_order_items", []).expect("drop purchase_order_items");
    }

    repo.create(CreatePurchaseOrder {
        supplier_id: supplier.id,
        items: vec![CreatePurchaseOrderItem {
            sku: "SKU-1".to_string(),
            name: "Test item".to_string(),
            quantity: Decimal::ONE,
            unit_cost: Decimal::ONE,
            ..Default::default()
        }],
        ..Default::default()
    })
    .expect_err("expected create purchase order to fail");

    let conn = db.conn().expect("get sqlite connection");
    let count: i64 = conn
        .query_row("SELECT COUNT(*) FROM purchase_orders", [], |row| row.get(0))
        .expect("count purchase_orders");
    assert_eq!(count, 0, "purchase order insert should have been rolled back");
}

#[test]
fn sqlite_shipment_create_rolls_back_when_item_insert_fails() {
    let db = SqliteDatabase::in_memory().expect("create in-memory sqlite db");
    {
        let conn = db.conn().expect("get sqlite connection");
        conn.execute("DROP TABLE shipment_items", []).expect("drop shipment_items");
    }

    let repo = db.shipments();
    repo.create(CreateShipment {
        order_id: Uuid::new_v4().into(),
        recipient_name: "Test recipient".to_string(),
        shipping_address: "123 Test St".to_string(),
        items: Some(vec![CreateShipmentItem {
            sku: "SKU-1".to_string(),
            name: "Test item".to_string(),
            quantity: 1,
            ..Default::default()
        }]),
        ..Default::default()
    })
    .expect_err("expected create shipment to fail");

    let conn = db.conn().expect("get sqlite connection");
    let count: i64 = conn
        .query_row("SELECT COUNT(*) FROM shipments", [], |row| row.get(0))
        .expect("count shipments");
    assert_eq!(count, 0, "shipment insert should have been rolled back");
}

#[test]
fn sqlite_inventory_adjust_rolls_back_when_transaction_insert_fails() {
    let db = SqliteDatabase::in_memory().expect("create in-memory sqlite db");
    let repo = db.inventory();

    let item = repo
        .create_item(CreateInventoryItem {
            sku: "SKU-1".to_string(),
            name: "Test item".to_string(),
            initial_quantity: Some(Decimal::new(10, 0)),
            ..Default::default()
        })
        .expect("create inventory item");

    {
        let conn = db.conn().expect("get sqlite connection");
        conn.execute("DROP TABLE inventory_transactions", []).expect("drop inventory_transactions");
    }

    repo.adjust(AdjustInventory {
        sku: "SKU-1".to_string(),
        quantity: Decimal::ONE,
        reason: "Test adjust".to_string(),
        location_id: None,
        reference_type: None,
        reference_id: None,
    })
    .expect_err("expected adjust to fail");

    let conn = db.conn().expect("get sqlite connection");
    let qty: String = conn
        .query_row(
            "SELECT quantity_on_hand FROM inventory_balances WHERE item_id = ?",
            [item.id],
            |row| row.get(0),
        )
        .expect("query inventory balance");
    let qty_dec: Decimal = qty.parse().expect("parse quantity_on_hand");
    assert_eq!(qty_dec, Decimal::new(10, 0), "balance should be unchanged");
}

#[test]
fn sqlite_inventory_release_reservation_rolls_back_when_balance_update_fails() {
    let db = SqliteDatabase::in_memory().expect("create in-memory sqlite db");
    let repo = db.inventory();

    repo.create_item(CreateInventoryItem {
        sku: "SKU-2".to_string(),
        name: "Test item".to_string(),
        initial_quantity: Some(Decimal::new(5, 0)),
        ..Default::default()
    })
    .expect("create inventory item");

    let reservation = repo
        .reserve(ReserveInventory {
            sku: "SKU-2".to_string(),
            quantity: Decimal::ONE,
            reference_type: "test".to_string(),
            reference_id: "ref-1".to_string(),
            location_id: None,
            expires_in_seconds: None,
        })
        .expect("reserve inventory");

    {
        let conn = db.conn().expect("get sqlite connection");
        conn.execute("DROP TABLE inventory_balances", []).expect("drop inventory_balances");
    }

    repo.release_reservation(reservation.id).expect_err("expected release_reservation to fail");

    let conn = db.conn().expect("get sqlite connection");
    let status: String = conn
        .query_row(
            "SELECT status FROM inventory_reservations WHERE id = ?",
            [reservation.id.to_string()],
            |row| row.get(0),
        )
        .expect("query reservation status");
    assert_eq!(status, "pending", "reservation status should be unchanged");
}