"""Smoke tests for Payment tracking feature""" import pytest from datetime import date, timedelta from decimal import Decimal from app import db from app.models import Payment, Invoice, User, Project, Client from factories import UserFactory, ClientFactory, ProjectFactory, InvoiceFactory, PaymentFactory @pytest.fixture def setup_payment_test_data(app): """Setup test data for payment smoke tests""" with app.app_context(): # Create user user = UserFactory() user.role = "admin" db.session.add(user) db.session.commit() # Create client client = ClientFactory() db.session.flush() # Create project project = ProjectFactory(client_id=client.id, billable=True, hourly_rate=Decimal("100.00")) db.session.flush() # Create invoice invoice = InvoiceFactory( project_id=project.id, client_name=client.name, client_id=client.id, created_by=user.id, due_date=(date.today() + timedelta(days=30)), ) invoice.subtotal = Decimal("1000.00") invoice.tax_rate = Decimal("21.00") invoice.tax_amount = Decimal("210.00") invoice.total_amount = Decimal("1210.00") db.session.add(invoice) db.session.commit() yield {"user": user, "client": client, "project": project, "invoice": invoice} # Cleanup Payment.query.filter_by(invoice_id=invoice.id).delete() db.session.delete(invoice) db.session.delete(project) db.session.delete(client) db.session.delete(user) db.session.commit() class TestPaymentSmokeTests: """Smoke tests to verify basic payment functionality""" def test_payment_model_exists(self): """Test that Payment model exists and is importable""" from app.models import Payment assert Payment is not None def test_payment_blueprint_registered(self, app): """Test that payments blueprint is registered""" with app.app_context(): assert "payments" in app.blueprints def test_payment_routes_exist(self, app): """Test that payment routes are registered""" with app.app_context(): rules = [rule.rule for rule in app.url_map.iter_rules()] assert "/payments" in rules assert any("/payments/" in rule for rule in rules) assert "/payments/create" in rules def test_payment_database_table_exists(self, app): """Test that payments table exists in database""" with app.app_context(): from sqlalchemy import inspect inspector = inspect(db.engine) tables = inspector.get_table_names() assert "payments" in tables def test_payment_model_columns(self, app): """Test that payment model has required columns""" with app.app_context(): from sqlalchemy import inspect inspector = inspect(db.engine) columns = [col["name"] for col in inspector.get_columns("payments")] # Required columns required_columns = [ "id", "invoice_id", "amount", "currency", "payment_date", "method", "reference", "notes", "status", "received_by", "gateway_transaction_id", "gateway_fee", "net_amount", "created_at", "updated_at", ] for col in required_columns: assert col in columns, f"Column '{col}' not found in payments table" def test_create_and_retrieve_payment(self, app, setup_payment_test_data): """Test creating and retrieving a payment""" with app.app_context(): invoice = setup_payment_test_data["invoice"] user = setup_payment_test_data["user"] # Create payment payment = PaymentFactory( invoice_id=invoice.id, amount=Decimal("500.00"), currency="EUR", payment_date=date.today(), method="bank_transfer", status="completed", received_by=user.id, ) db.session.add(payment) db.session.commit() payment_id = payment.id # Retrieve payment retrieved_payment = Payment.query.get(payment_id) assert retrieved_payment is not None assert retrieved_payment.amount == Decimal("500.00") assert retrieved_payment.invoice_id == invoice.id # Cleanup db.session.delete(payment) db.session.commit() def test_payment_invoice_relationship(self, app, setup_payment_test_data): """Test relationship between payment and invoice""" with app.app_context(): invoice_id = setup_payment_test_data["invoice"].id # Re-query invoice to attach to current session from app.models.invoice import Invoice invoice = Invoice.query.get(invoice_id) # Create payment payment = PaymentFactory( invoice_id=invoice.id, amount=Decimal("500.00"), currency="EUR", payment_date=date.today(), status="completed", ) db.session.add(payment) db.session.commit() # Test relationship assert payment.invoice is not None assert payment.invoice.id == invoice.id # Refresh invoice to get updated relationships db.session.refresh(invoice) assert payment in invoice.payments # Cleanup db.session.delete(payment) db.session.commit() def test_payment_list_page_loads(self, client, setup_payment_test_data): """Test that payment list page loads""" with client: user = setup_payment_test_data["user"] # Login client.post("/login", data={"username": user.username}, follow_redirects=True) # Access payments list response = client.get("/payments") assert response.status_code == 200 def test_payment_create_page_loads(self, client, setup_payment_test_data): """Test that payment create page loads""" with client: user = setup_payment_test_data["user"] # Login client.post("/login", data={"username": user.username}, follow_redirects=True) # Access payment create page response = client.get("/payments/create") assert response.status_code == 200 def test_payment_workflow_end_to_end(self, client, app, setup_payment_test_data): """Test complete payment workflow from creation to viewing""" with client: user = setup_payment_test_data["user"] invoice = setup_payment_test_data["invoice"] # Login client.post("/login", data={"username": user.username}, follow_redirects=True) # Create payment payment_data = { "invoice_id": invoice.id, "amount": "500.00", "currency": "EUR", "payment_date": date.today().strftime("%Y-%m-%d"), "method": "bank_transfer", "reference": "SMOKE-TEST-001", "status": "completed", "notes": "Smoke test payment", } create_response = client.post("/payments/create", data=payment_data, follow_redirects=True) assert create_response.status_code == 200 # Verify payment was created in database (client context already provides app context) payment = Payment.query.filter_by(reference="SMOKE-TEST-001").first() assert payment is not None payment_id = payment.id # View payment view_response = client.get(f"/payments/{payment_id}") assert view_response.status_code == 200 # Cleanup db.session.delete(payment) db.session.commit() def test_payment_templates_exist(self, app): """Test that payment templates exist""" import os template_dir = os.path.join(app.root_path, "templates", "payments") assert os.path.exists(template_dir), "Payments template directory does not exist" required_templates = ["list.html", "create.html", "edit.html", "view.html"] for template in required_templates: template_path = os.path.join(template_dir, template) assert os.path.exists(template_path), f"Template {template} does not exist" def test_payment_model_methods(self, app, setup_payment_test_data): """Test that payment model has required methods""" with app.app_context(): invoice = setup_payment_test_data["invoice"] payment = PaymentFactory( invoice_id=invoice.id, amount=Decimal("500.00"), currency="EUR", payment_date=date.today(), gateway_fee=Decimal("15.00"), status="completed", ) # Test calculate_net_amount method assert hasattr(payment, "calculate_net_amount") payment.calculate_net_amount() assert payment.net_amount == Decimal("485.00") # Test to_dict method assert hasattr(payment, "to_dict") payment_dict = payment.to_dict() assert isinstance(payment_dict, dict) assert "amount" in payment_dict assert "invoice_id" in payment_dict def test_payment_filter_functionality(self, client, app, setup_payment_test_data): """Test payment filtering functionality""" with client: user = setup_payment_test_data["user"] invoice = setup_payment_test_data["invoice"] # Login client.post("/login", data={"username": user.username}, follow_redirects=True) # Create test payments with different statuses (client context already provides app context) payment1 = PaymentFactory( invoice_id=invoice.id, amount=Decimal("100.00"), currency="EUR", payment_date=date.today(), method="cash", status="completed", ) payment2 = PaymentFactory( invoice_id=invoice.id, amount=Decimal("200.00"), currency="EUR", payment_date=date.today(), method="bank_transfer", status="pending", ) db.session.add_all([payment1, payment2]) db.session.commit() # Test filter by status response = client.get("/payments?status=completed") assert response.status_code == 200 # Test filter by method response = client.get("/payments?method=cash") assert response.status_code == 200 # Cleanup db.session.delete(payment1) db.session.delete(payment2) db.session.commit() @pytest.mark.skip(reason="SQLAlchemy compile error - needs investigation") def test_invoice_shows_payment_history(self, client, app, setup_payment_test_data): """Test that invoice view shows payment history""" with client: user = setup_payment_test_data["user"] invoice = setup_payment_test_data["invoice"] # Login client.post("/login", data={"username": user.username}, follow_redirects=True) # Create payment (client context already provides app context) payment = Payment( invoice_id=invoice.id, amount=Decimal("500.00"), currency="EUR", payment_date=date.today(), status="completed", ) db.session.add(payment) db.session.commit() # View invoice response = client.get(f"/invoices/{invoice.id}") assert response.status_code == 200 # Check if payment history section exists in response assert b"Payment History" in response.data or b"payment" in response.data.lower() # Cleanup db.session.delete(payment) db.session.commit() class TestPaymentFeatureCompleteness: """Tests to ensure payment feature is complete""" def test_migration_exists(self): """Test that payment migration file exists""" import os migration_dir = os.path.join(os.path.dirname(__file__), "..", "migrations", "versions") migration_files = os.listdir(migration_dir) # Check for payment-related migration payment_migrations = [f for f in migration_files if "payment" in f.lower()] assert len(payment_migrations) > 0, "No payment migration found" def test_payment_api_endpoint_exists(self, app): """Test that payment API endpoints exist""" with app.app_context(): rules = [rule.rule for rule in app.url_map.iter_rules()] assert any("payments" in rule and "api" in rule for rule in rules) def test_all_crud_operations_work(self, client, app, setup_payment_test_data): """Test that all CRUD operations for payments work""" with client: user = setup_payment_test_data["user"] invoice = setup_payment_test_data["invoice"] # Login client.post("/login", data={"username": user.username}, follow_redirects=True) # CREATE payment_data = { "invoice_id": invoice.id, "amount": "300.00", "currency": "EUR", "payment_date": date.today().strftime("%Y-%m-%d"), "method": "cash", "status": "completed", } create_response = client.post("/payments/create", data=payment_data, follow_redirects=True) assert create_response.status_code == 200 # Query payment (client context already provides app context) payment = Payment.query.filter_by(invoice_id=invoice.id, method="cash").first() assert payment is not None payment_id = payment.id # READ read_response = client.get(f"/payments/{payment_id}") assert read_response.status_code == 200 # UPDATE update_data = { "amount": "350.00", "currency": "EUR", "payment_date": date.today().strftime("%Y-%m-%d"), "method": "bank_transfer", "status": "completed", } update_response = client.post(f"/payments/{payment_id}/edit", data=update_data, follow_redirects=True) assert update_response.status_code == 200 # Verify update db.session.refresh(payment) assert payment.amount == Decimal("350.00") assert payment.method == "bank_transfer" # DELETE delete_response = client.post(f"/payments/{payment_id}/delete", follow_redirects=True) assert delete_response.status_code == 200 # Verify deletion deleted_payment = Payment.query.get(payment_id) assert deleted_payment is None