""" Comprehensive Test Suite for Mirchi Trading Backend Tests all scenarios: - Awaak regular transactions - Jawaak regular transactions - Patti Awaak (no lot/weight) - Patti Jawaak (with lot/weight - mandatory) - Mixed bills (normal + patti items) - Payments - Reverts - Party rename with historical tracking - Multilingual (Marathi, Hindi, English) inputs """ import pytest import asyncio from httpx import AsyncClient, ASGITransport from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker from sqlalchemy import select from datetime import datetime import sys import os # Add backend to path sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from app.main import app from app.core.database import Base, get_db from app.models import Party, MirchiType, Transaction, PattiTransaction, BillNumberSequence from app.models.transaction import BillType from app.models.patti_transaction import PattiBillType from app.models.party import PartyType # Test database URL (use SQLite for tests) TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:" @pytest.fixture(scope="session") def event_loop(): """Create event loop for async tests""" loop = asyncio.get_event_loop_policy().new_event_loop() yield loop loop.close() @pytest.fixture(scope="function") async def db_session(): """Create database session for tests""" engine = create_async_engine(TEST_DATABASE_URL, echo=False) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) async_session = async_sessionmaker(engine, expire_on_commit=False) async with async_session() as session: yield session await engine.dispose() @pytest.fixture(scope="function") async def client(db_session): """Create test client""" async def override_get_db(): yield db_session app.dependency_overrides[get_db] = override_get_db async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: yield ac app.dependency_overrides.clear() # ============================================ # PARTY TESTS # ============================================ class TestParty: """Tests for party operations""" @pytest.mark.asyncio async def test_create_party_english(self, client: AsyncClient): """Test creating party with English name""" response = await client.post("/api/parties", json={ "name": "Ramesh Kumar", "phone": "9876543210", "city": "Mumbai", "party_type": "awaak", "notes": "Regular customer" }) assert response.status_code == 201 data = response.json() assert data["success"] is True assert data["data"]["name"] == "Ramesh Kumar" assert data["data"]["party_type"] == "awaak" @pytest.mark.asyncio async def test_create_party_marathi(self, client: AsyncClient): """Test creating party with Marathi name""" response = await client.post("/api/parties", json={ "name": "रमेश कुमार", "phone": "9876543210", "city": "मुंबई", "party_type": "jawaak", "notes": "नियमित ग्राहक" }) assert response.status_code == 201 data = response.json() assert data["data"]["name"] == "रमेश कुमार" assert data["data"]["city"] == "मुंबई" @pytest.mark.asyncio async def test_create_party_hindi(self, client: AsyncClient): """Test creating party with Hindi name""" response = await client.post("/api/parties", json={ "name": "राम लाल", "phone": "9876543211", "city": "दिल्ली", "party_type": "both", "notes": "Hindi customer" }) assert response.status_code == 201 data = response.json() assert data["data"]["name"] == "राम लाल" @pytest.mark.asyncio async def test_rename_party(self, client: AsyncClient): """Test renaming party - should track history""" # Create party create_response = await client.post("/api/parties", json={ "name": "Old Name", "phone": "9876543212", "city": "Pune", "party_type": "awaak" }) party_id = create_response.json()["data"]["id"] # Rename party rename_response = await client.put(f"/api/parties/{party_id}", json={ "name": "New Name" }) assert rename_response.status_code == 200 data = rename_response.json() assert data["data"]["name"] == "New Name" # Check name history history_response = await client.get(f"/api/parties/{party_id}/name-history") assert history_response.status_code == 200 history = history_response.json() assert history["current_name"] == "New Name" assert len(history["name_history"]) == 1 assert history["name_history"][0]["old_name"] == "Old Name" assert history["name_history"][0]["new_name"] == "New Name" # ============================================ # BILL NUMBER TESTS # ============================================ class TestBillNumbering: """Tests for bill numbering system""" @pytest.mark.asyncio async def test_get_next_bill_number_awaak(self, client: AsyncClient): """Test getting next bill number for awaak""" response = await client.get("/api/transactions/next-bill-number?bill_type=awaak") assert response.status_code == 200 data = response.json() assert data["success"] is True # Format: A2025A001 assert data["bill_number"].startswith("A") assert "A" in data["bill_number"] # Batch A @pytest.mark.asyncio async def test_get_next_bill_number_jawaak(self, client: AsyncClient): """Test getting next bill number for jawaak""" response = await client.get("/api/transactions/next-bill-number?bill_type=jawaak") assert response.status_code == 200 data = response.json() assert data["bill_number"].startswith("J") @pytest.mark.asyncio async def test_get_next_bill_number_patti_awaak(self, client: AsyncClient): """Test getting next bill number for patti_awaak""" response = await client.get("/api/patti-transactions/next-bill-number?bill_type=patti_awaak") assert response.status_code == 200 data = response.json() assert data["bill_number"].startswith("PA") @pytest.mark.asyncio async def test_get_next_bill_number_patti_jawaak(self, client: AsyncClient): """Test getting next bill number for patti_jawaak""" response = await client.get("/api/patti-transactions/next-bill-number?bill_type=patti_jawaak") assert response.status_code == 200 data = response.json() assert data["bill_number"].startswith("PJ") @pytest.mark.asyncio async def test_bill_number_info(self, client: AsyncClient): """Test getting bill number sequence info""" response = await client.get("/api/transactions/bill-number-info?bill_type=awaak") assert response.status_code == 200 data = response.json() assert "bill_type" in data assert "year" in data assert "prefix" in data assert "batches" in data assert "total_used" in data assert "total_remaining" in data # ============================================ # TRANSACTION TESTS # ============================================ class TestTransactions: """Tests for regular transactions""" @pytest.mark.asyncio async def test_create_awaak_transaction(self, client: AsyncClient): """Test creating awaak (purchase) transaction""" # Create party first party_response = await client.post("/api/parties", json={ "name": "Test Party", "phone": "9876543213", "city": "Nagpur", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] # Create transaction response = await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 10000, "total_amount": 10500, "paid_amount": 5000, "balance_amount": 5500, "lot_number": "LOT001", "items": [ { "mirchi_name": "Byadgi", "poti_count": 10, "net_weight": 500, "rate_per_kg": 20, "amount": 10000, "source": "normal" } ], "payments": [ { "amount": 5000, "mode": "cash", "reference": "Payment 1" } ] }) assert response.status_code == 201 data = response.json() assert data["success"] is True assert data["data"]["bill_type"] == "awaak" assert data["data"]["bill_number"].startswith("A") assert data["data"]["party_name"] == "Test Party" assert data["data"]["party_name_current"] == "Test Party" @pytest.mark.asyncio async def test_create_jawaak_transaction(self, client: AsyncClient): """Test creating jawaak (sales) transaction""" # Create party first party_response = await client.post("/api/parties", json={ "name": "Sales Party", "phone": "9876543214", "city": "Solapur", "party_type": "jawaak" }) party_id = party_response.json()["data"]["id"] # Create transaction response = await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "jawaak", "party_id": party_id, "subtotal": 15000, "total_amount": 15500, "paid_amount": 15500, "balance_amount": 0, "items": [ { "mirchi_name": "Sankeshwari", "poti_count": 15, "net_weight": 750, "rate_per_kg": 20, "amount": 15000, "source": "normal" } ], "payments": [ { "amount": 15500, "mode": "upi", "reference": "UPI123" } ] }) assert response.status_code == 201 data = response.json() assert data["data"]["bill_type"] == "jawaak" assert data["data"]["bill_number"].startswith("J") @pytest.mark.asyncio async def test_create_mixed_bill(self, client: AsyncClient): """Test creating mixed bill (normal + patti items)""" # Create party party_response = await client.post("/api/parties", json={ "name": "Mixed Party", "phone": "9876543215", "city": "Kolhapur", "party_type": "both" }) party_id = party_response.json()["data"]["id"] # Create mixed transaction response = await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 20000, "total_amount": 20500, "paid_amount": 10000, "balance_amount": 10500, "items": [ { "mirchi_name": "Byadgi", "poti_count": 10, "net_weight": 500, "rate_per_kg": 20, "amount": 10000, "source": "normal" }, { "mirchi_name": "Sankeshwari", "poti_count": 10, "net_weight": 500, "rate_per_kg": 20, "amount": 10000, "source": "patti" } ], "payments": [ { "amount": 10000, "mode": "bank", "reference": "BANK001" } ] }) assert response.status_code == 201 data = response.json() assert data["data"]["is_mixed"] is True @pytest.mark.asyncio async def test_transaction_with_marathi_names(self, client: AsyncClient): """Test transaction with Marathi mirchi names""" # Create party with Marathi name party_response = await client.post("/api/parties", json={ "name": "मराठी पक्ष", "phone": "9876543216", "city": "सांगली", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] # Create transaction with Marathi mirchi name response = await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 12000, "total_amount": 12500, "paid_amount": 12000, "balance_amount": 500, "items": [ { "mirchi_name": "ब्याडगी मिरची", "poti_count": 12, "net_weight": 600, "rate_per_kg": 20, "amount": 12000, "source": "normal" } ], "payments": [ { "amount": 12000, "mode": "cash" } ] }) assert response.status_code == 201 data = response.json() assert data["data"]["party_name"] == "मराठी पक्ष" assert data["data"]["items"][0]["mirchi_name"] == "ब्याडगी मिरची" # ============================================ # PATTI TRANSACTION TESTS # ============================================ class TestPattiTransactions: """Tests for patti transactions""" @pytest.mark.asyncio async def test_create_patti_awaak(self, client: AsyncClient): """Test creating patti awaak (no lot/weight initially)""" # Create party party_response = await client.post("/api/parties", json={ "name": "Patti Holder", "phone": "9876543217", "city": "Latur", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] # Create patti awaak transaction (no lot required) response = await client.post("/api/patti-transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "patti_awaak", "party_id": party_id, "subtotal": 8000, "total_amount": 8500, "paid_amount": 4000, "balance_amount": 4500, "expenses": { "packing": 100, "godown": 200, "commission": 100, "hamali": 50, "gaadi_bhade": 50 }, "items": [ { "mirchi_name": "Local Mirchi", "poti_count": 8, "net_weight": 400, "rate_per_kg": 20, "amount": 8000 } ] }) assert response.status_code == 201 data = response.json() assert data["data"]["bill_type"] == "patti_awaak" assert data["data"]["bill_number"].startswith("PA") # lot_number is optional for patti_awaak assert data["data"].get("lot_number") is None @pytest.mark.asyncio async def test_create_patti_jawaak_with_lot(self, client: AsyncClient): """Test creating patti jawaak (lot mandatory)""" # Create party party_response = await client.post("/api/parties", json={ "name": "Patti Buyer", "phone": "9876543218", "city": "Osmanabad", "party_type": "jawaak" }) party_id = party_response.json()["data"]["id"] # Create patti jawaak transaction (lot required) response = await client.post("/api/patti-transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "patti_jawaak", "party_id": party_id, "subtotal": 12000, "total_amount": 12500, "paid_amount": 12000, "balance_amount": 500, "lot_number": "LOT-PJ-001", # MANDATORY for patti_jawaak "gross_weight": 650, "verified_net_weight": 600, "expenses": { "packing": 150, "godown": 100, "commission": 150, "hamali": 50, "gaadi_bhade": 50 }, "items": [ { "mirchi_name": "Premium Mirchi", "poti_count": 12, "net_weight": 600, "rate_per_kg": 20, "amount": 12000 } ] }) assert response.status_code == 201 data = response.json() assert data["data"]["bill_type"] == "patti_jawaak" assert data["data"]["bill_number"].startswith("PJ") assert data["data"]["lot_number"] == "LOT-PJ-001" assert data["data"]["gross_weight"] == 650 assert data["data"]["verified_net_weight"] == 600 @pytest.mark.asyncio async def test_patti_jawaak_without_lot_fails(self, client: AsyncClient): """Test that patti jawaak without lot_number fails""" # Create party party_response = await client.post("/api/parties", json={ "name": "Test Party", "phone": "9876543219", "city": "Beed", "party_type": "jawaak" }) party_id = party_response.json()["data"]["id"] # Try to create patti jawaak without lot (should fail) response = await client.post("/api/patti-transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "patti_jawaak", "party_id": party_id, "subtotal": 10000, "total_amount": 10500, "paid_amount": 10000, "balance_amount": 500, # lot_number is MISSING - should fail "items": [ { "mirchi_name": "Test Mirchi", "poti_count": 10, "net_weight": 500, "rate_per_kg": 20, "amount": 10000 } ] }) assert response.status_code == 400 assert "lot number" in response.json()["detail"].lower() # ============================================ # PAYMENT TESTS # ============================================ class TestPayments: """Tests for payment operations""" @pytest.mark.asyncio async def test_update_payment(self, client: AsyncClient): """Test updating payment for transaction""" # Create party and transaction party_response = await client.post("/api/parties", json={ "name": "Payment Test Party", "phone": "9876543220", "city": "Aurangabad", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] txn_response = await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 10000, "total_amount": 10000, "paid_amount": 5000, "balance_amount": 5000, "items": [ { "mirchi_name": "Test", "poti_count": 10, "net_weight": 500, "rate_per_kg": 20, "amount": 10000, "source": "normal" } ], "payments": [ {"amount": 5000, "mode": "cash"} ] }) txn_id = txn_response.json()["data"]["id"] # Update payment response = await client.post(f"/api/transactions/{txn_id}/payment?amount=3000") assert response.status_code == 200 data = response.json() assert data["data"]["paid_amount"] == 8000 assert data["data"]["balance_amount"] == 2000 # ============================================ # REVERT TESTS # ============================================ class TestReverts: """Tests for revert operations""" @pytest.mark.asyncio async def test_revert_transaction(self, client: AsyncClient): """Test reverting a transaction""" # Create party and transaction party_response = await client.post("/api/parties", json={ "name": "Revert Test Party", "phone": "9876543221", "city": "Nanded", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] txn_response = await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 5000, "total_amount": 5000, "paid_amount": 0, "balance_amount": 5000, "items": [ { "mirchi_name": "Test", "poti_count": 5, "net_weight": 250, "rate_per_kg": 20, "amount": 5000, "source": "normal" } ], "payments": [] }) txn_id = txn_response.json()["data"]["id"] # Revert transaction response = await client.post(f"/api/transactions/{txn_id}/revert") assert response.status_code == 200 data = response.json() assert data["success"] is True @pytest.mark.asyncio async def test_revert_patti_transaction(self, client: AsyncClient): """Test reverting a patti transaction""" # Create party and patti transaction party_response = await client.post("/api/parties", json={ "name": "Patti Revert Test", "phone": "9876543222", "city": "Parbhani", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] txn_response = await client.post("/api/patti-transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "patti_awaak", "party_id": party_id, "subtotal": 6000, "total_amount": 6000, "paid_amount": 0, "balance_amount": 6000, "items": [ { "mirchi_name": "Test", "poti_count": 6, "net_weight": 300, "rate_per_kg": 20, "amount": 6000 } ] }) txn_id = txn_response.json()["data"]["id"] # Revert patti transaction response = await client.post(f"/api/patti-transactions/{txn_id}/revert") assert response.status_code == 200 # ============================================ # SEARCH TESTS # ============================================ class TestSearch: """Tests for search functionality""" @pytest.mark.asyncio async def test_search_by_party_name(self, client: AsyncClient): """Test searching transactions by party name""" # Create party with unique name party_response = await client.post("/api/parties", json={ "name": "Unique Search Party XYZ", "phone": "9876543223", "city": "Jalna", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] # Create transaction await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 1000, "total_amount": 1000, "paid_amount": 1000, "balance_amount": 0, "items": [ { "mirchi_name": "Test", "poti_count": 1, "net_weight": 50, "rate_per_kg": 20, "amount": 1000, "source": "normal" } ], "payments": [{"amount": 1000, "mode": "cash"}] }) # Search by party name response = await client.get("/api/transactions?search=Unique Search Party") assert response.status_code == 200 data = response.json() assert data["total"] >= 1 @pytest.mark.asyncio async def test_search_marathi_text(self, client: AsyncClient): """Test searching with Marathi text""" # Create party with Marathi name party_response = await client.post("/api/parties", json={ "name": "शोध पक्ष", "phone": "9876543224", "city": "मुंबई", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] # Create transaction await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 1000, "total_amount": 1000, "paid_amount": 1000, "balance_amount": 0, "items": [ { "mirchi_name": "मिरची", "poti_count": 1, "net_weight": 50, "rate_per_kg": 20, "amount": 1000, "source": "normal" } ], "payments": [{"amount": 1000, "mode": "cash"}] }) # Search with Marathi text response = await client.get("/api/transactions?search=शोध") assert response.status_code == 200 # ============================================ # PARTY RENAME IMPACT TESTS # ============================================ class TestPartyRenameImpact: """Tests for party rename impact on transactions""" @pytest.mark.asyncio async def test_rename_updates_transaction_search(self, client: AsyncClient): """Test that renaming party updates party_name_current in transactions""" # Create party party_response = await client.post("/api/parties", json={ "name": "Original Name", "phone": "9876543225", "city": "Pune", "party_type": "awaak" }) party_id = party_response.json()["data"]["id"] # Create transaction txn_response = await client.post("/api/transactions", json={ "bill_date": datetime.now().strftime("%Y-%m-%d"), "bill_type": "awaak", "party_id": party_id, "subtotal": 1000, "total_amount": 1000, "paid_amount": 1000, "balance_amount": 0, "items": [ { "mirchi_name": "Test", "poti_count": 1, "net_weight": 50, "rate_per_kg": 20, "amount": 1000, "source": "normal" } ], "payments": [{"amount": 1000, "mode": "cash"}] }) txn_id = txn_response.json()["data"]["id"] # Rename party await client.put(f"/api/parties/{party_id}", json={"name": "Renamed Party"}) # Get transaction - should have updated party_name_current response = await client.get(f"/api/transactions/{txn_id}") assert response.status_code == 200 data = response.json() # Original name snapshot should remain assert data["data"]["party_name"] == "Original Name" # Current name should be updated for search assert data["data"]["party_name_current"] == "Renamed Party" # Search by new name should find the transaction search_response = await client.get("/api/transactions?search=Renamed Party") assert search_response.status_code == 200 assert search_response.json()["total"] >= 1 if __name__ == "__main__": pytest.main([__file__, "-v", "--tb=short"])