Initial commit - production deployment
This commit is contained in:
244
services/sales/tests/conftest.py
Normal file
244
services/sales/tests/conftest.py
Normal file
@@ -0,0 +1,244 @@
|
||||
# services/sales/tests/conftest.py
|
||||
"""
|
||||
Pytest configuration and fixtures for Sales Service tests
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from typing import AsyncGenerator
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
from app.core.config import settings
|
||||
from app.core.database import Base, get_db
|
||||
from app.models.sales import SalesData
|
||||
from app.schemas.sales import SalesDataCreate
|
||||
|
||||
|
||||
# Test database configuration
|
||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create event loop for the test session"""
|
||||
loop = asyncio.new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_engine():
|
||||
"""Create test database engine"""
|
||||
engine = create_async_engine(
|
||||
TEST_DATABASE_URL,
|
||||
poolclass=StaticPool,
|
||||
connect_args={"check_same_thread": False}
|
||||
)
|
||||
|
||||
# Create tables
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
|
||||
yield engine
|
||||
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_db_session(test_engine) -> AsyncGenerator[AsyncSession, None]:
|
||||
"""Create test database session"""
|
||||
async_session = async_sessionmaker(
|
||||
test_engine, class_=AsyncSession, expire_on_commit=False
|
||||
)
|
||||
|
||||
async with async_session() as session:
|
||||
yield session
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
"""Create test client"""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def override_get_db(test_db_session):
|
||||
"""Override get_db dependency for testing"""
|
||||
async def _override_get_db():
|
||||
yield test_db_session
|
||||
|
||||
app.dependency_overrides[get_db] = _override_get_db
|
||||
yield
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# Test data fixtures
|
||||
@pytest.fixture
|
||||
def sample_tenant_id() -> UUID:
|
||||
"""Sample tenant ID for testing"""
|
||||
return uuid4()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_sales_data(sample_tenant_id: UUID) -> SalesDataCreate:
|
||||
"""Sample sales data for testing"""
|
||||
return SalesDataCreate(
|
||||
date=datetime.now(timezone.utc),
|
||||
inventory_product_id="550e8400-e29b-41d4-a716-446655440000",
|
||||
product_name="Pan Integral",
|
||||
product_category="Panadería",
|
||||
product_sku="PAN001",
|
||||
quantity_sold=5,
|
||||
unit_price=Decimal("2.50"),
|
||||
revenue=Decimal("12.50"),
|
||||
cost_of_goods=Decimal("6.25"),
|
||||
discount_applied=Decimal("0"),
|
||||
location_id="STORE_001",
|
||||
sales_channel="in_store",
|
||||
source="manual",
|
||||
notes="Test sale",
|
||||
weather_condition="sunny",
|
||||
is_holiday=False,
|
||||
is_weekend=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_sales_records(sample_tenant_id: UUID) -> list[dict]:
|
||||
"""Multiple sample sales records"""
|
||||
base_date = datetime.now(timezone.utc)
|
||||
return [
|
||||
{
|
||||
"tenant_id": sample_tenant_id,
|
||||
"date": base_date,
|
||||
"inventory_product_id": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"product_name": "Croissant",
|
||||
"quantity_sold": 3,
|
||||
"revenue": Decimal("7.50"),
|
||||
"location_id": "STORE_001",
|
||||
"source": "manual"
|
||||
},
|
||||
{
|
||||
"tenant_id": sample_tenant_id,
|
||||
"date": base_date,
|
||||
"inventory_product_id": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"product_name": "Café Americano",
|
||||
"quantity_sold": 2,
|
||||
"revenue": Decimal("5.00"),
|
||||
"location_id": "STORE_001",
|
||||
"source": "pos"
|
||||
},
|
||||
{
|
||||
"tenant_id": sample_tenant_id,
|
||||
"date": base_date,
|
||||
"inventory_product_id": "550e8400-e29b-41d4-a716-446655440003",
|
||||
"product_name": "Bocadillo Jamón",
|
||||
"quantity_sold": 1,
|
||||
"revenue": Decimal("4.50"),
|
||||
"location_id": "STORE_002",
|
||||
"source": "manual"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_csv_data() -> str:
|
||||
"""Sample CSV data for import testing"""
|
||||
return """date,product,quantity,revenue,location
|
||||
2024-01-15,Pan Integral,5,12.50,STORE_001
|
||||
2024-01-15,Croissant,3,7.50,STORE_001
|
||||
2024-01-15,Café Americano,2,5.00,STORE_002
|
||||
2024-01-16,Pan de Molde,8,16.00,STORE_001
|
||||
2024-01-16,Magdalenas,6,9.00,STORE_002"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_json_data() -> str:
|
||||
"""Sample JSON data for import testing"""
|
||||
return """[
|
||||
{
|
||||
"date": "2024-01-15",
|
||||
"product": "Pan Integral",
|
||||
"quantity": 5,
|
||||
"revenue": 12.50,
|
||||
"location": "STORE_001"
|
||||
},
|
||||
{
|
||||
"date": "2024-01-15",
|
||||
"product": "Croissant",
|
||||
"quantity": 3,
|
||||
"revenue": 7.50,
|
||||
"location": "STORE_001"
|
||||
}
|
||||
]"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def populated_db(test_db_session: AsyncSession, sample_sales_records: list[dict]):
|
||||
"""Database populated with test data"""
|
||||
for record_data in sample_sales_records:
|
||||
sales_record = SalesData(**record_data)
|
||||
test_db_session.add(sales_record)
|
||||
|
||||
await test_db_session.commit()
|
||||
yield test_db_session
|
||||
|
||||
|
||||
# Mock fixtures for external dependencies
|
||||
@pytest.fixture
|
||||
def mock_messaging():
|
||||
"""Mock messaging service"""
|
||||
class MockMessaging:
|
||||
def __init__(self):
|
||||
self.published_events = []
|
||||
|
||||
async def publish_sales_created(self, data):
|
||||
self.published_events.append(("sales_created", data))
|
||||
return True
|
||||
|
||||
async def publish_data_imported(self, data):
|
||||
self.published_events.append(("data_imported", data))
|
||||
return True
|
||||
|
||||
return MockMessaging()
|
||||
|
||||
|
||||
# Performance testing fixtures
|
||||
@pytest.fixture
|
||||
def large_csv_data() -> str:
|
||||
"""Large CSV data for performance testing"""
|
||||
headers = "date,product,quantity,revenue,location\n"
|
||||
rows = []
|
||||
|
||||
for i in range(1000): # 1000 records
|
||||
rows.append(f"2024-01-{(i % 30) + 1:02d},Producto_{i % 10},1,2.50,STORE_{i % 3 + 1:03d}")
|
||||
|
||||
return headers + "\n".join(rows)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def performance_test_data(sample_tenant_id: UUID) -> list[dict]:
|
||||
"""Large dataset for performance testing"""
|
||||
records = []
|
||||
base_date = datetime.now(timezone.utc)
|
||||
|
||||
for i in range(500): # 500 records
|
||||
records.append({
|
||||
"tenant_id": sample_tenant_id,
|
||||
"date": base_date,
|
||||
"inventory_product_id": f"550e8400-e29b-41d4-a716-{i:012x}",
|
||||
"product_name": f"Test Product {i % 20}",
|
||||
"quantity_sold": (i % 10) + 1,
|
||||
"revenue": Decimal(str(((i % 10) + 1) * 2.5)),
|
||||
"location_id": f"STORE_{(i % 5) + 1:03d}",
|
||||
"source": "test"
|
||||
})
|
||||
|
||||
return records
|
||||
417
services/sales/tests/integration/test_api_endpoints.py
Normal file
417
services/sales/tests/integration/test_api_endpoints.py
Normal file
@@ -0,0 +1,417 @@
|
||||
# services/sales/tests/integration/test_api_endpoints.py
|
||||
"""
|
||||
Integration tests for Sales API endpoints
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSalesAPIEndpoints:
|
||||
"""Test Sales API endpoints integration"""
|
||||
|
||||
async def test_create_sales_record_success(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test creating a sales record via API"""
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "Pan Integral",
|
||||
"product_category": "Panadería",
|
||||
"quantity_sold": 5,
|
||||
"unit_price": 2.50,
|
||||
"revenue": 12.50,
|
||||
"location_id": "STORE_001",
|
||||
"sales_channel": "in_store",
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["product_name"] == "Pan Integral"
|
||||
assert data["quantity_sold"] == 5
|
||||
assert "id" in data
|
||||
|
||||
async def test_create_sales_record_validation_error(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test creating a sales record with validation errors"""
|
||||
invalid_data = {
|
||||
"date": "invalid-date",
|
||||
"product_name": "", # Empty product name
|
||||
"quantity_sold": -1, # Invalid quantity
|
||||
"revenue": -5.00 # Invalid revenue
|
||||
}
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=invalid_data
|
||||
)
|
||||
|
||||
assert response.status_code == 422 # Validation error
|
||||
|
||||
async def test_get_sales_records(self, test_client, override_get_db, sample_tenant_id, populated_db):
|
||||
"""Test getting sales records for tenant"""
|
||||
response = test_client.get(f"/api/v1/sales?tenant_id={sample_tenant_id}")
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 0
|
||||
|
||||
async def test_get_sales_records_with_filters(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test getting sales records with filters"""
|
||||
# First create a record
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "Croissant",
|
||||
"quantity_sold": 3,
|
||||
"revenue": 7.50,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
create_response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
|
||||
# Get with product filter
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}&product_name=Croissant"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) >= 1
|
||||
assert all(record["product_name"] == "Croissant" for record in data)
|
||||
|
||||
async def test_get_sales_record_by_id(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test getting a specific sales record"""
|
||||
# First create a record
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "Test Product",
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
create_response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
created_record = create_response.json()
|
||||
|
||||
# Get the specific record
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/{created_record['id']}?tenant_id={sample_tenant_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == created_record["id"]
|
||||
assert data["product_name"] == "Test Product"
|
||||
|
||||
async def test_get_sales_record_not_found(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test getting a non-existent sales record"""
|
||||
fake_id = str(uuid4())
|
||||
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/{fake_id}?tenant_id={sample_tenant_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_update_sales_record(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test updating a sales record"""
|
||||
# First create a record
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "Original Product",
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
create_response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
created_record = create_response.json()
|
||||
|
||||
# Update the record
|
||||
update_data = {
|
||||
"product_name": "Updated Product",
|
||||
"quantity_sold": 2,
|
||||
"revenue": 10.00
|
||||
}
|
||||
|
||||
response = test_client.put(
|
||||
f"/api/v1/sales/{created_record['id']}?tenant_id={sample_tenant_id}",
|
||||
json=update_data
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["product_name"] == "Updated Product"
|
||||
assert data["quantity_sold"] == 2
|
||||
|
||||
async def test_delete_sales_record(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test deleting a sales record"""
|
||||
# First create a record
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "To Delete",
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
create_response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
created_record = create_response.json()
|
||||
|
||||
# Delete the record
|
||||
response = test_client.delete(
|
||||
f"/api/v1/sales/{created_record['id']}?tenant_id={sample_tenant_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify it's deleted
|
||||
get_response = test_client.get(
|
||||
f"/api/v1/sales/{created_record['id']}?tenant_id={sample_tenant_id}"
|
||||
)
|
||||
assert get_response.status_code == 404
|
||||
|
||||
async def test_get_sales_analytics(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test getting sales analytics"""
|
||||
# First create some records
|
||||
for i in range(3):
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": f"Product {i}",
|
||||
"quantity_sold": i + 1,
|
||||
"revenue": (i + 1) * 5.0,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Get analytics
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/analytics?tenant_id={sample_tenant_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "total_revenue" in data
|
||||
assert "total_quantity" in data
|
||||
assert "total_transactions" in data
|
||||
|
||||
async def test_validate_sales_record(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test validating a sales record"""
|
||||
# First create a record
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "To Validate",
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
create_response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
created_record = create_response.json()
|
||||
|
||||
# Validate the record
|
||||
validation_data = {
|
||||
"validation_notes": "Validated by manager"
|
||||
}
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales/{created_record['id']}/validate?tenant_id={sample_tenant_id}",
|
||||
json=validation_data
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["is_validated"] is True
|
||||
assert data["validation_notes"] == "Validated by manager"
|
||||
|
||||
async def test_get_product_sales(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test getting sales for specific product"""
|
||||
# First create records for different products
|
||||
products = ["Product A", "Product B", "Product A"]
|
||||
for product in products:
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": product,
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Get sales for Product A
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/products/Product A?tenant_id={sample_tenant_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 2 # Two Product A records
|
||||
assert all(record["product_name"] == "Product A" for record in data)
|
||||
|
||||
async def test_get_product_categories(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test getting product categories"""
|
||||
# First create records with categories
|
||||
for category in ["Panadería", "Cafetería", "Panadería"]:
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "Test Product",
|
||||
"product_category": category,
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Get categories
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/categories?tenant_id={sample_tenant_id}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
async def test_export_sales_data_csv(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test exporting sales data as CSV"""
|
||||
# First create some records
|
||||
for i in range(3):
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": f"Export Product {i}",
|
||||
"quantity_sold": i + 1,
|
||||
"revenue": (i + 1) * 5.0,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={sample_tenant_id}",
|
||||
json=sales_data
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Export as CSV
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/export?tenant_id={sample_tenant_id}&format=csv"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.headers["content-type"].startswith("text/csv")
|
||||
assert "Export Product" in response.text
|
||||
|
||||
async def test_bulk_create_sales_records(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test bulk creating sales records"""
|
||||
bulk_data = [
|
||||
{
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": f"Bulk Product {i}",
|
||||
"quantity_sold": i + 1,
|
||||
"revenue": (i + 1) * 3.0,
|
||||
"source": "bulk"
|
||||
}
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
response = test_client.post(
|
||||
f"/api/v1/sales/bulk?tenant_id={sample_tenant_id}",
|
||||
json=bulk_data
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["created_count"] == 5
|
||||
assert data["success"] is True
|
||||
|
||||
async def test_tenant_isolation(self, test_client, override_get_db):
|
||||
"""Test that tenants can only access their own data"""
|
||||
tenant_1 = uuid4()
|
||||
tenant_2 = uuid4()
|
||||
|
||||
# Create record for tenant 1
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "Tenant 1 Product",
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00,
|
||||
"source": "manual"
|
||||
}
|
||||
|
||||
create_response = test_client.post(
|
||||
f"/api/v1/sales?tenant_id={tenant_1}",
|
||||
json=sales_data
|
||||
)
|
||||
assert create_response.status_code == 201
|
||||
created_record = create_response.json()
|
||||
|
||||
# Try to access with tenant 2
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/{created_record['id']}?tenant_id={tenant_2}"
|
||||
)
|
||||
|
||||
assert response.status_code == 404 # Should not be found
|
||||
|
||||
# Tenant 1 should still be able to access
|
||||
response = test_client.get(
|
||||
f"/api/v1/sales/{created_record['id']}?tenant_id={tenant_1}"
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_api_error_handling(self, test_client, override_get_db, sample_tenant_id):
|
||||
"""Test API error handling"""
|
||||
# Test missing tenant_id
|
||||
sales_data = {
|
||||
"date": datetime.now(timezone.utc).isoformat(),
|
||||
"product_name": "Test Product",
|
||||
"quantity_sold": 1,
|
||||
"revenue": 5.00
|
||||
}
|
||||
|
||||
response = test_client.post("/api/v1/sales", json=sales_data)
|
||||
assert response.status_code == 422 # Missing required parameter
|
||||
|
||||
# Test invalid UUID
|
||||
response = test_client.get("/api/v1/sales/invalid-uuid?tenant_id={sample_tenant_id}")
|
||||
assert response.status_code == 422 # Invalid UUID format
|
||||
10
services/sales/tests/requirements.txt
Normal file
10
services/sales/tests/requirements.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
# Testing dependencies for Sales Service
|
||||
pytest==7.4.3
|
||||
pytest-asyncio==0.21.1
|
||||
pytest-mock==3.12.0
|
||||
httpx==0.25.2
|
||||
fastapi[all]==0.104.1
|
||||
sqlalchemy[asyncio]==2.0.23
|
||||
aiosqlite==0.19.0
|
||||
pandas==2.1.4
|
||||
coverage==7.3.2
|
||||
96
services/sales/tests/unit/test_batch.py
Normal file
96
services/sales/tests/unit/test_batch.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from datetime import date
|
||||
import uuid
|
||||
|
||||
from app.main import app
|
||||
from app.api.batch import SalesSummaryBatchRequest, SalesSummary
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sales_service():
|
||||
with patch("app.api.batch.get_sales_service") as mock:
|
||||
service = AsyncMock()
|
||||
mock.return_value = service
|
||||
yield service
|
||||
|
||||
@pytest.fixture
|
||||
def mock_current_user():
|
||||
with patch("app.api.batch.get_current_user_dep") as mock:
|
||||
mock.return_value = {
|
||||
"user_id": str(uuid.uuid4()),
|
||||
"role": "admin",
|
||||
"tenant_id": str(uuid.uuid4())
|
||||
}
|
||||
yield mock
|
||||
|
||||
def test_get_sales_summary_batch_success(mock_sales_service, mock_current_user):
|
||||
# Setup
|
||||
tenant_id_1 = str(uuid.uuid4())
|
||||
tenant_id_2 = str(uuid.uuid4())
|
||||
|
||||
request_data = {
|
||||
"tenant_ids": [tenant_id_1, tenant_id_2],
|
||||
"start_date": "2025-01-01",
|
||||
"end_date": "2025-01-31"
|
||||
}
|
||||
|
||||
# Mock service response
|
||||
mock_sales_service.get_sales_analytics.side_effect = [
|
||||
{
|
||||
"total_revenue": 1000.0,
|
||||
"total_orders": 10,
|
||||
"average_order_value": 100.0
|
||||
},
|
||||
{
|
||||
"total_revenue": 2000.0,
|
||||
"total_orders": 20,
|
||||
"average_order_value": 100.0
|
||||
}
|
||||
]
|
||||
|
||||
# Execute
|
||||
response = client.post("/api/v1/batch/sales-summary", json=request_data)
|
||||
|
||||
# Verify
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert len(data) == 2
|
||||
assert data[tenant_id_1]["total_revenue"] == 1000.0
|
||||
assert data[tenant_id_2]["total_revenue"] == 2000.0
|
||||
|
||||
# Verify service calls
|
||||
assert mock_sales_service.get_sales_analytics.call_count == 2
|
||||
|
||||
def test_get_sales_summary_batch_empty(mock_sales_service, mock_current_user):
|
||||
# Setup
|
||||
request_data = {
|
||||
"tenant_ids": [],
|
||||
"start_date": "2025-01-01",
|
||||
"end_date": "2025-01-31"
|
||||
}
|
||||
|
||||
# Execute
|
||||
response = client.post("/api/v1/batch/sales-summary", json=request_data)
|
||||
|
||||
# Verify
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {}
|
||||
|
||||
def test_get_sales_summary_batch_limit_exceeded(mock_sales_service, mock_current_user):
|
||||
# Setup
|
||||
tenant_ids = [str(uuid.uuid4()) for _ in range(101)]
|
||||
request_data = {
|
||||
"tenant_ids": tenant_ids,
|
||||
"start_date": "2025-01-01",
|
||||
"end_date": "2025-01-31"
|
||||
}
|
||||
|
||||
# Execute
|
||||
response = client.post("/api/v1/batch/sales-summary", json=request_data)
|
||||
|
||||
# Verify
|
||||
assert response.status_code == 400
|
||||
assert "Maximum 100 tenant IDs allowed" in response.json()["detail"]
|
||||
384
services/sales/tests/unit/test_data_import.py
Normal file
384
services/sales/tests/unit/test_data_import.py
Normal file
@@ -0,0 +1,384 @@
|
||||
# services/sales/tests/unit/test_data_import.py
|
||||
"""
|
||||
Unit tests for Data Import Service
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import base64
|
||||
from decimal import Decimal
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from app.services.data_import_service import DataImportService, SalesValidationResult, SalesImportResult
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestDataImportService:
|
||||
"""Test Data Import Service functionality"""
|
||||
|
||||
@pytest.fixture
|
||||
def import_service(self):
|
||||
"""Create data import service instance"""
|
||||
return DataImportService()
|
||||
|
||||
async def test_validate_csv_import_data_valid(self, import_service, sample_tenant_id, sample_csv_data):
|
||||
"""Test validation of valid CSV import data"""
|
||||
data = {
|
||||
"tenant_id": str(sample_tenant_id),
|
||||
"data": sample_csv_data,
|
||||
"data_format": "csv"
|
||||
}
|
||||
|
||||
result = await import_service.validate_import_data(data)
|
||||
|
||||
assert result.is_valid is True
|
||||
assert result.total_records == 5
|
||||
assert len(result.errors) == 0
|
||||
assert result.summary["status"] == "valid"
|
||||
|
||||
async def test_validate_csv_import_data_missing_tenant(self, import_service, sample_csv_data):
|
||||
"""Test validation with missing tenant_id"""
|
||||
data = {
|
||||
"data": sample_csv_data,
|
||||
"data_format": "csv"
|
||||
}
|
||||
|
||||
result = await import_service.validate_import_data(data)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert any(error["code"] == "MISSING_TENANT_ID" for error in result.errors)
|
||||
|
||||
async def test_validate_csv_import_data_empty_file(self, import_service, sample_tenant_id):
|
||||
"""Test validation with empty file"""
|
||||
data = {
|
||||
"tenant_id": str(sample_tenant_id),
|
||||
"data": "",
|
||||
"data_format": "csv"
|
||||
}
|
||||
|
||||
result = await import_service.validate_import_data(data)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert any(error["code"] == "EMPTY_FILE" for error in result.errors)
|
||||
|
||||
async def test_validate_csv_import_data_unsupported_format(self, import_service, sample_tenant_id):
|
||||
"""Test validation with unsupported format"""
|
||||
data = {
|
||||
"tenant_id": str(sample_tenant_id),
|
||||
"data": "some data",
|
||||
"data_format": "unsupported"
|
||||
}
|
||||
|
||||
result = await import_service.validate_import_data(data)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert any(error["code"] == "UNSUPPORTED_FORMAT" for error in result.errors)
|
||||
|
||||
async def test_validate_csv_missing_required_columns(self, import_service, sample_tenant_id):
|
||||
"""Test validation with missing required columns"""
|
||||
invalid_csv = "invalid_column,another_invalid\nvalue1,value2"
|
||||
data = {
|
||||
"tenant_id": str(sample_tenant_id),
|
||||
"data": invalid_csv,
|
||||
"data_format": "csv"
|
||||
}
|
||||
|
||||
result = await import_service.validate_import_data(data)
|
||||
|
||||
assert result.is_valid is False
|
||||
assert any(error["code"] == "MISSING_DATE_COLUMN" for error in result.errors)
|
||||
assert any(error["code"] == "MISSING_PRODUCT_COLUMN" for error in result.errors)
|
||||
|
||||
async def test_process_csv_import_success(self, import_service, sample_tenant_id, sample_csv_data):
|
||||
"""Test successful CSV import processing"""
|
||||
with patch('app.services.data_import_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_repository.create_sales_record.return_value = AsyncMock()
|
||||
|
||||
with patch('app.services.data_import_service.SalesRepository', return_value=mock_repository):
|
||||
result = await import_service.process_import(
|
||||
sample_tenant_id,
|
||||
sample_csv_data,
|
||||
"csv",
|
||||
"test.csv"
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.records_processed == 5
|
||||
assert result.records_created == 5
|
||||
assert result.records_failed == 0
|
||||
|
||||
async def test_process_json_import_success(self, import_service, sample_tenant_id, sample_json_data):
|
||||
"""Test successful JSON import processing"""
|
||||
with patch('app.services.data_import_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_repository.create_sales_record.return_value = AsyncMock()
|
||||
|
||||
with patch('app.services.data_import_service.SalesRepository', return_value=mock_repository):
|
||||
result = await import_service.process_import(
|
||||
sample_tenant_id,
|
||||
sample_json_data,
|
||||
"json",
|
||||
"test.json"
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.records_processed == 2
|
||||
assert result.records_created == 2
|
||||
|
||||
async def test_process_excel_import_base64(self, import_service, sample_tenant_id):
|
||||
"""Test Excel import with base64 encoded data"""
|
||||
# Create a simple Excel-like data structure
|
||||
excel_data = json.dumps([{
|
||||
"date": "2024-01-15",
|
||||
"product": "Pan Integral",
|
||||
"quantity": 5,
|
||||
"revenue": 12.50
|
||||
}])
|
||||
|
||||
# Encode as base64
|
||||
encoded_data = "data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64," + \
|
||||
base64.b64encode(excel_data.encode()).decode()
|
||||
|
||||
with patch('app.services.data_import_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_repository.create_sales_record.return_value = AsyncMock()
|
||||
|
||||
# Mock pandas.read_excel to avoid dependency issues
|
||||
with patch('pandas.read_excel') as mock_read_excel:
|
||||
import pandas as pd
|
||||
mock_df = pd.DataFrame([{
|
||||
"date": "2024-01-15",
|
||||
"product": "Pan Integral",
|
||||
"quantity": 5,
|
||||
"revenue": 12.50
|
||||
}])
|
||||
mock_read_excel.return_value = mock_df
|
||||
|
||||
with patch('app.services.data_import_service.SalesRepository', return_value=mock_repository):
|
||||
result = await import_service.process_import(
|
||||
sample_tenant_id,
|
||||
encoded_data,
|
||||
"excel",
|
||||
"test.xlsx"
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.records_created == 1
|
||||
|
||||
async def test_detect_columns_mapping(self, import_service):
|
||||
"""Test column detection and mapping"""
|
||||
columns = ["fecha", "producto", "cantidad", "ingresos", "tienda"]
|
||||
|
||||
mapping = import_service._detect_columns(columns)
|
||||
|
||||
assert mapping["date"] == "fecha"
|
||||
assert mapping["product"] == "producto"
|
||||
assert mapping["quantity"] == "cantidad"
|
||||
assert mapping["revenue"] == "ingresos"
|
||||
assert mapping["location"] == "tienda"
|
||||
|
||||
async def test_parse_date_multiple_formats(self, import_service):
|
||||
"""Test date parsing with different formats"""
|
||||
# Test various date formats
|
||||
dates_to_test = [
|
||||
"2024-01-15",
|
||||
"15/01/2024",
|
||||
"01/15/2024",
|
||||
"15-01-2024",
|
||||
"2024/01/15",
|
||||
"2024-01-15 10:30:00"
|
||||
]
|
||||
|
||||
for date_str in dates_to_test:
|
||||
result = import_service._parse_date(date_str)
|
||||
assert result is not None
|
||||
assert isinstance(result, datetime)
|
||||
|
||||
async def test_parse_date_invalid_formats(self, import_service):
|
||||
"""Test date parsing with invalid formats"""
|
||||
invalid_dates = ["invalid", "not-a-date", "", None, "32/13/2024"]
|
||||
|
||||
for date_str in invalid_dates:
|
||||
result = import_service._parse_date(date_str)
|
||||
assert result is None
|
||||
|
||||
async def test_clean_product_name(self, import_service):
|
||||
"""Test product name cleaning"""
|
||||
test_cases = [
|
||||
(" pan de molde ", "Pan De Molde"),
|
||||
("café con leche!!!", "Café Con Leche"),
|
||||
("té verde orgánico", "Té Verde Orgánico"),
|
||||
("bocadillo de jamón", "Bocadillo De Jamón"),
|
||||
("", "Producto sin nombre"),
|
||||
(None, "Producto sin nombre")
|
||||
]
|
||||
|
||||
for input_name, expected in test_cases:
|
||||
result = import_service._clean_product_name(input_name)
|
||||
assert result == expected
|
||||
|
||||
async def test_parse_row_data_valid(self, import_service):
|
||||
"""Test parsing valid row data"""
|
||||
row = {
|
||||
"fecha": "2024-01-15",
|
||||
"producto": "Pan Integral",
|
||||
"cantidad": "5",
|
||||
"ingresos": "12.50",
|
||||
"tienda": "STORE_001"
|
||||
}
|
||||
|
||||
column_mapping = {
|
||||
"date": "fecha",
|
||||
"product": "producto",
|
||||
"quantity": "cantidad",
|
||||
"revenue": "ingresos",
|
||||
"location": "tienda"
|
||||
}
|
||||
|
||||
result = await import_service._parse_row_data(row, column_mapping, 1)
|
||||
|
||||
assert result["skip"] is False
|
||||
assert result["product_name"] == "Pan Integral"
|
||||
assert "inventory_product_id" in result # Should be generated during parsing
|
||||
assert result["quantity_sold"] == 5
|
||||
assert result["revenue"] == 12.5
|
||||
assert result["location_id"] == "STORE_001"
|
||||
|
||||
async def test_parse_row_data_missing_required(self, import_service):
|
||||
"""Test parsing row data with missing required fields"""
|
||||
row = {
|
||||
"producto": "Pan Integral",
|
||||
"cantidad": "5"
|
||||
# Missing date
|
||||
}
|
||||
|
||||
column_mapping = {
|
||||
"date": "fecha",
|
||||
"product": "producto",
|
||||
"quantity": "cantidad"
|
||||
}
|
||||
|
||||
result = await import_service._parse_row_data(row, column_mapping, 1)
|
||||
|
||||
assert result["skip"] is True
|
||||
assert len(result["errors"]) > 0
|
||||
assert "Missing date" in result["errors"][0]
|
||||
|
||||
async def test_parse_row_data_invalid_quantity(self, import_service):
|
||||
"""Test parsing row data with invalid quantity"""
|
||||
row = {
|
||||
"fecha": "2024-01-15",
|
||||
"producto": "Pan Integral",
|
||||
"cantidad": "invalid_quantity"
|
||||
}
|
||||
|
||||
column_mapping = {
|
||||
"date": "fecha",
|
||||
"product": "producto",
|
||||
"quantity": "cantidad"
|
||||
}
|
||||
|
||||
result = await import_service._parse_row_data(row, column_mapping, 1)
|
||||
|
||||
assert result["skip"] is False # Should not skip, just use default
|
||||
assert result["quantity_sold"] == 1 # Default quantity
|
||||
assert len(result["warnings"]) > 0
|
||||
|
||||
async def test_structure_messages(self, import_service):
|
||||
"""Test message structuring"""
|
||||
messages = [
|
||||
"Simple string message",
|
||||
{
|
||||
"type": "existing_dict",
|
||||
"message": "Already structured",
|
||||
"code": "TEST_CODE"
|
||||
}
|
||||
]
|
||||
|
||||
result = import_service._structure_messages(messages)
|
||||
|
||||
assert len(result) == 2
|
||||
assert result[0]["type"] == "general_message"
|
||||
assert result[0]["message"] == "Simple string message"
|
||||
assert result[1]["type"] == "existing_dict"
|
||||
|
||||
async def test_generate_suggestions_valid_file(self, import_service):
|
||||
"""Test suggestion generation for valid files"""
|
||||
validation_result = SalesValidationResult(
|
||||
is_valid=True,
|
||||
total_records=50,
|
||||
valid_records=50,
|
||||
invalid_records=0,
|
||||
errors=[],
|
||||
warnings=[],
|
||||
summary={}
|
||||
)
|
||||
|
||||
suggestions = import_service._generate_suggestions(validation_result, "csv", 0)
|
||||
|
||||
assert "El archivo está listo para procesamiento" in suggestions
|
||||
assert "Se procesarán aproximadamente 50 registros" in suggestions
|
||||
|
||||
async def test_generate_suggestions_large_file(self, import_service):
|
||||
"""Test suggestion generation for large files"""
|
||||
validation_result = SalesValidationResult(
|
||||
is_valid=True,
|
||||
total_records=2000,
|
||||
valid_records=2000,
|
||||
invalid_records=0,
|
||||
errors=[],
|
||||
warnings=[],
|
||||
summary={}
|
||||
)
|
||||
|
||||
suggestions = import_service._generate_suggestions(validation_result, "csv", 0)
|
||||
|
||||
assert "Archivo grande: el procesamiento puede tomar varios minutos" in suggestions
|
||||
|
||||
async def test_import_error_handling(self, import_service, sample_tenant_id):
|
||||
"""Test import error handling"""
|
||||
# Test with unsupported format
|
||||
with pytest.raises(ValueError, match="Unsupported format"):
|
||||
await import_service.process_import(
|
||||
sample_tenant_id,
|
||||
"some data",
|
||||
"unsupported_format"
|
||||
)
|
||||
|
||||
async def test_performance_large_import(self, import_service, sample_tenant_id, large_csv_data):
|
||||
"""Test performance with large CSV import"""
|
||||
with patch('app.services.data_import_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_repository.create_sales_record.return_value = AsyncMock()
|
||||
|
||||
with patch('app.services.data_import_service.SalesRepository', return_value=mock_repository):
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
result = await import_service.process_import(
|
||||
sample_tenant_id,
|
||||
large_csv_data,
|
||||
"csv",
|
||||
"large_test.csv"
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
assert result.success is True
|
||||
assert result.records_processed == 1000
|
||||
assert execution_time < 10.0 # Should complete in under 10 seconds
|
||||
215
services/sales/tests/unit/test_repositories.py
Normal file
215
services/sales/tests/unit/test_repositories.py
Normal file
@@ -0,0 +1,215 @@
|
||||
# services/sales/tests/unit/test_repositories.py
|
||||
"""
|
||||
Unit tests for Sales Repository
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
|
||||
from app.repositories.sales_repository import SalesRepository
|
||||
from app.models.sales import SalesData
|
||||
from app.schemas.sales import SalesDataCreate, SalesDataUpdate, SalesDataQuery
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSalesRepository:
|
||||
"""Test Sales Repository operations"""
|
||||
|
||||
async def test_create_sales_record(self, test_db_session, sample_tenant_id, sample_sales_data):
|
||||
"""Test creating a sales record"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
record = await repository.create_sales_record(sample_sales_data, sample_tenant_id)
|
||||
|
||||
assert record is not None
|
||||
assert record.id is not None
|
||||
assert record.tenant_id == sample_tenant_id
|
||||
assert record.inventory_product_id == sample_sales_data.inventory_product_id
|
||||
assert record.quantity_sold == sample_sales_data.quantity_sold
|
||||
assert record.revenue == sample_sales_data.revenue
|
||||
|
||||
async def test_get_by_id(self, test_db_session, sample_tenant_id, sample_sales_data):
|
||||
"""Test getting a sales record by ID"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
# Create record first
|
||||
created_record = await repository.create_sales_record(sample_sales_data, sample_tenant_id)
|
||||
|
||||
# Get by ID
|
||||
retrieved_record = await repository.get_by_id(created_record.id)
|
||||
|
||||
assert retrieved_record is not None
|
||||
assert retrieved_record.id == created_record.id
|
||||
assert retrieved_record.inventory_product_id == created_record.inventory_product_id
|
||||
|
||||
async def test_get_by_tenant(self, populated_db, sample_tenant_id):
|
||||
"""Test getting records by tenant"""
|
||||
repository = SalesRepository(populated_db)
|
||||
|
||||
records = await repository.get_by_tenant(sample_tenant_id)
|
||||
|
||||
assert len(records) == 3 # From populated_db fixture
|
||||
assert all(record.tenant_id == sample_tenant_id for record in records)
|
||||
|
||||
async def test_get_by_product(self, populated_db, sample_tenant_id):
|
||||
"""Test getting records by product"""
|
||||
repository = SalesRepository(populated_db)
|
||||
|
||||
# Get by inventory_product_id instead of product name
|
||||
test_product_id = "550e8400-e29b-41d4-a716-446655440001"
|
||||
records = await repository.get_by_inventory_product_id(sample_tenant_id, test_product_id)
|
||||
|
||||
assert len(records) == 1
|
||||
assert records[0].inventory_product_id == test_product_id
|
||||
|
||||
async def test_update_record(self, test_db_session, sample_tenant_id, sample_sales_data):
|
||||
"""Test updating a sales record"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
# Create record first
|
||||
created_record = await repository.create_sales_record(sample_sales_data, sample_tenant_id)
|
||||
|
||||
# Update record
|
||||
update_data = SalesDataUpdate(
|
||||
inventory_product_id="550e8400-e29b-41d4-a716-446655440999",
|
||||
product_name="Updated Product",
|
||||
quantity_sold=10,
|
||||
revenue=Decimal("25.00")
|
||||
)
|
||||
|
||||
updated_record = await repository.update(created_record.id, update_data.model_dump(exclude_unset=True))
|
||||
|
||||
assert updated_record.inventory_product_id == "550e8400-e29b-41d4-a716-446655440999"
|
||||
assert updated_record.quantity_sold == 10
|
||||
assert updated_record.revenue == Decimal("25.00")
|
||||
|
||||
async def test_delete_record(self, test_db_session, sample_tenant_id, sample_sales_data):
|
||||
"""Test deleting a sales record"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
# Create record first
|
||||
created_record = await repository.create_sales_record(sample_sales_data, sample_tenant_id)
|
||||
|
||||
# Delete record
|
||||
success = await repository.delete(created_record.id)
|
||||
|
||||
assert success is True
|
||||
|
||||
# Verify record is deleted
|
||||
deleted_record = await repository.get_by_id(created_record.id)
|
||||
assert deleted_record is None
|
||||
|
||||
async def test_get_analytics(self, populated_db, sample_tenant_id):
|
||||
"""Test getting analytics for tenant"""
|
||||
repository = SalesRepository(populated_db)
|
||||
|
||||
analytics = await repository.get_analytics(sample_tenant_id)
|
||||
|
||||
assert "total_revenue" in analytics
|
||||
assert "total_quantity" in analytics
|
||||
assert "total_transactions" in analytics
|
||||
assert "average_transaction_value" in analytics
|
||||
assert analytics["total_transactions"] == 3
|
||||
|
||||
async def test_get_product_categories(self, populated_db, sample_tenant_id):
|
||||
"""Test getting distinct product categories"""
|
||||
repository = SalesRepository(populated_db)
|
||||
|
||||
categories = await repository.get_product_categories(sample_tenant_id)
|
||||
|
||||
assert isinstance(categories, list)
|
||||
# Should be empty since populated_db doesn't set categories
|
||||
|
||||
async def test_validate_record(self, test_db_session, sample_tenant_id, sample_sales_data):
|
||||
"""Test validating a sales record"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
# Create record first
|
||||
created_record = await repository.create_sales_record(sample_sales_data, sample_tenant_id)
|
||||
|
||||
# Validate record
|
||||
validated_record = await repository.validate_record(created_record.id, "Test validation")
|
||||
|
||||
assert validated_record.is_validated is True
|
||||
assert validated_record.validation_notes == "Test validation"
|
||||
|
||||
async def test_query_with_filters(self, populated_db, sample_tenant_id):
|
||||
"""Test querying with filters"""
|
||||
repository = SalesRepository(populated_db)
|
||||
|
||||
query = SalesDataQuery(
|
||||
inventory_product_id="550e8400-e29b-41d4-a716-446655440001",
|
||||
limit=10,
|
||||
offset=0
|
||||
)
|
||||
|
||||
records = await repository.get_by_tenant(sample_tenant_id, query)
|
||||
|
||||
assert len(records) == 1
|
||||
assert records[0].inventory_product_id == "550e8400-e29b-41d4-a716-446655440001"
|
||||
|
||||
async def test_bulk_create(self, test_db_session, sample_tenant_id):
|
||||
"""Test bulk creating records"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
# Create multiple records data
|
||||
bulk_data = [
|
||||
{
|
||||
"date": datetime.now(timezone.utc),
|
||||
"inventory_product_id": f"550e8400-e29b-41d4-a716-{i+100:012x}",
|
||||
"product_name": f"Product {i}",
|
||||
"quantity_sold": i + 1,
|
||||
"revenue": Decimal(str((i + 1) * 2.5)),
|
||||
"source": "bulk_test"
|
||||
}
|
||||
for i in range(5)
|
||||
]
|
||||
|
||||
created_count = await repository.bulk_create_sales_data(bulk_data, sample_tenant_id)
|
||||
|
||||
assert created_count == 5
|
||||
|
||||
# Verify records were created
|
||||
all_records = await repository.get_by_tenant(sample_tenant_id)
|
||||
assert len(all_records) == 5
|
||||
|
||||
async def test_repository_error_handling(self, test_db_session, sample_tenant_id):
|
||||
"""Test repository error handling"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
# Test getting non-existent record
|
||||
non_existent = await repository.get_by_id("non-existent-id")
|
||||
assert non_existent is None
|
||||
|
||||
# Test deleting non-existent record
|
||||
delete_success = await repository.delete("non-existent-id")
|
||||
assert delete_success is False
|
||||
|
||||
async def test_performance_bulk_operations(self, test_db_session, sample_tenant_id, performance_test_data):
|
||||
"""Test performance of bulk operations"""
|
||||
repository = SalesRepository(test_db_session)
|
||||
|
||||
# Test bulk create performance
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
created_count = await repository.bulk_create_sales_data(performance_test_data, sample_tenant_id)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
assert created_count == len(performance_test_data)
|
||||
assert execution_time < 5.0 # Should complete in under 5 seconds
|
||||
|
||||
# Test bulk retrieval performance
|
||||
start_time = time.time()
|
||||
|
||||
all_records = await repository.get_by_tenant(sample_tenant_id)
|
||||
|
||||
end_time = time.time()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
assert len(all_records) == len(performance_test_data)
|
||||
assert execution_time < 2.0 # Should complete in under 2 seconds
|
||||
290
services/sales/tests/unit/test_services.py
Normal file
290
services/sales/tests/unit/test_services.py
Normal file
@@ -0,0 +1,290 @@
|
||||
# services/sales/tests/unit/test_services.py
|
||||
"""
|
||||
Unit tests for Sales Service
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from uuid import uuid4
|
||||
|
||||
from app.services.sales_service import SalesService
|
||||
from app.schemas.sales import SalesDataCreate, SalesDataUpdate, SalesDataQuery
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSalesService:
|
||||
"""Test Sales Service business logic"""
|
||||
|
||||
@pytest.fixture
|
||||
def sales_service(self):
|
||||
"""Create sales service instance"""
|
||||
return SalesService()
|
||||
|
||||
async def test_create_sales_record_success(self, sales_service, sample_tenant_id, sample_sales_data):
|
||||
"""Test successful sales record creation"""
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_record = AsyncMock()
|
||||
mock_record.id = uuid4()
|
||||
mock_record.inventory_product_id = sample_sales_data.inventory_product_id
|
||||
mock_repository.create_sales_record.return_value = mock_record
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.create_sales_record(
|
||||
sample_sales_data,
|
||||
sample_tenant_id
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert result.id is not None
|
||||
mock_repository.create_sales_record.assert_called_once_with(sample_sales_data, sample_tenant_id)
|
||||
|
||||
async def test_create_sales_record_validation_error(self, sales_service, sample_tenant_id):
|
||||
"""Test sales record creation with validation error"""
|
||||
# Create invalid sales data (future date)
|
||||
invalid_data = SalesDataCreate(
|
||||
date=datetime(2030, 1, 1, tzinfo=timezone.utc), # Future date
|
||||
inventory_product_id="550e8400-e29b-41d4-a716-446655440000",
|
||||
product_name="Test Product",
|
||||
quantity_sold=1,
|
||||
revenue=Decimal("5.00")
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Sales date cannot be in the future"):
|
||||
await sales_service.create_sales_record(invalid_data, sample_tenant_id)
|
||||
|
||||
async def test_update_sales_record(self, sales_service, sample_tenant_id):
|
||||
"""Test updating a sales record"""
|
||||
record_id = uuid4()
|
||||
update_data = SalesDataUpdate(
|
||||
inventory_product_id="550e8400-e29b-41d4-a716-446655440999",
|
||||
product_name="Updated Product",
|
||||
quantity_sold=10
|
||||
)
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
|
||||
# Mock existing record
|
||||
mock_existing = AsyncMock()
|
||||
mock_existing.tenant_id = sample_tenant_id
|
||||
mock_repository.get_by_id.return_value = mock_existing
|
||||
|
||||
# Mock updated record
|
||||
mock_updated = AsyncMock()
|
||||
mock_updated.inventory_product_id = "550e8400-e29b-41d4-a716-446655440999"
|
||||
mock_repository.update.return_value = mock_updated
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.update_sales_record(
|
||||
record_id,
|
||||
update_data,
|
||||
sample_tenant_id
|
||||
)
|
||||
|
||||
assert result.inventory_product_id == "550e8400-e29b-41d4-a716-446655440999"
|
||||
mock_repository.update.assert_called_once()
|
||||
|
||||
async def test_update_nonexistent_record(self, sales_service, sample_tenant_id):
|
||||
"""Test updating a non-existent record"""
|
||||
record_id = uuid4()
|
||||
update_data = SalesDataUpdate(inventory_product_id="550e8400-e29b-41d4-a716-446655440999", product_name="Updated Product")
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_repository.get_by_id.return_value = None # Record not found
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
with pytest.raises(ValueError, match="not found for tenant"):
|
||||
await sales_service.update_sales_record(
|
||||
record_id,
|
||||
update_data,
|
||||
sample_tenant_id
|
||||
)
|
||||
|
||||
async def test_get_sales_records(self, sales_service, sample_tenant_id):
|
||||
"""Test getting sales records for tenant"""
|
||||
query_params = SalesDataQuery(limit=10)
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_records = [AsyncMock(), AsyncMock()]
|
||||
mock_repository.get_by_tenant.return_value = mock_records
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.get_sales_records(
|
||||
sample_tenant_id,
|
||||
query_params
|
||||
)
|
||||
|
||||
assert len(result) == 2
|
||||
mock_repository.get_by_tenant.assert_called_once_with(sample_tenant_id, query_params)
|
||||
|
||||
async def test_get_sales_record_success(self, sales_service, sample_tenant_id):
|
||||
"""Test getting a specific sales record"""
|
||||
record_id = uuid4()
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_record = AsyncMock()
|
||||
mock_record.tenant_id = sample_tenant_id
|
||||
mock_repository.get_by_id.return_value = mock_record
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.get_sales_record(record_id, sample_tenant_id)
|
||||
|
||||
assert result is not None
|
||||
assert result.tenant_id == sample_tenant_id
|
||||
|
||||
async def test_get_sales_record_wrong_tenant(self, sales_service, sample_tenant_id):
|
||||
"""Test getting a record that belongs to different tenant"""
|
||||
record_id = uuid4()
|
||||
wrong_tenant_id = uuid4()
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_record = AsyncMock()
|
||||
mock_record.tenant_id = sample_tenant_id # Different tenant
|
||||
mock_repository.get_by_id.return_value = mock_record
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.get_sales_record(record_id, wrong_tenant_id)
|
||||
|
||||
assert result is None
|
||||
|
||||
async def test_delete_sales_record(self, sales_service, sample_tenant_id):
|
||||
"""Test deleting a sales record"""
|
||||
record_id = uuid4()
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
|
||||
# Mock existing record
|
||||
mock_existing = AsyncMock()
|
||||
mock_existing.tenant_id = sample_tenant_id
|
||||
mock_repository.get_by_id.return_value = mock_existing
|
||||
|
||||
mock_repository.delete.return_value = True
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.delete_sales_record(record_id, sample_tenant_id)
|
||||
|
||||
assert result is True
|
||||
mock_repository.delete.assert_called_once_with(record_id)
|
||||
|
||||
async def test_get_product_sales(self, sales_service, sample_tenant_id):
|
||||
"""Test getting sales for specific product"""
|
||||
inventory_product_id = "550e8400-e29b-41d4-a716-446655440000"
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_records = [AsyncMock(), AsyncMock()]
|
||||
mock_repository.get_by_product.return_value = mock_records
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.get_product_sales(sample_tenant_id, inventory_product_id)
|
||||
|
||||
assert len(result) == 2
|
||||
mock_repository.get_by_product.assert_called_once()
|
||||
|
||||
async def test_get_sales_analytics(self, sales_service, sample_tenant_id):
|
||||
"""Test getting sales analytics"""
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
mock_analytics = {
|
||||
"total_revenue": Decimal("100.00"),
|
||||
"total_quantity": 50,
|
||||
"total_transactions": 10
|
||||
}
|
||||
mock_repository.get_analytics.return_value = mock_analytics
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.get_sales_analytics(sample_tenant_id)
|
||||
|
||||
assert result["total_revenue"] == Decimal("100.00")
|
||||
assert result["total_quantity"] == 50
|
||||
assert result["total_transactions"] == 10
|
||||
|
||||
async def test_validate_sales_record(self, sales_service, sample_tenant_id):
|
||||
"""Test validating a sales record"""
|
||||
record_id = uuid4()
|
||||
validation_notes = "Validated by manager"
|
||||
|
||||
with patch('app.services.sales_service.get_db_transaction') as mock_get_db:
|
||||
mock_db = AsyncMock()
|
||||
mock_get_db.return_value.__aenter__.return_value = mock_db
|
||||
|
||||
mock_repository = AsyncMock()
|
||||
|
||||
# Mock existing record
|
||||
mock_existing = AsyncMock()
|
||||
mock_existing.tenant_id = sample_tenant_id
|
||||
mock_repository.get_by_id.return_value = mock_existing
|
||||
|
||||
# Mock validated record
|
||||
mock_validated = AsyncMock()
|
||||
mock_validated.is_validated = True
|
||||
mock_repository.validate_record.return_value = mock_validated
|
||||
|
||||
with patch('app.services.sales_service.SalesRepository', return_value=mock_repository):
|
||||
result = await sales_service.validate_sales_record(
|
||||
record_id,
|
||||
sample_tenant_id,
|
||||
validation_notes
|
||||
)
|
||||
|
||||
assert result.is_validated is True
|
||||
mock_repository.validate_record.assert_called_once_with(record_id, validation_notes)
|
||||
|
||||
async def test_validate_sales_data_business_rules(self, sales_service, sample_tenant_id):
|
||||
"""Test business validation rules"""
|
||||
# Test revenue mismatch detection
|
||||
sales_data = SalesDataCreate(
|
||||
date=datetime.now(timezone.utc),
|
||||
inventory_product_id="550e8400-e29b-41d4-a716-446655440000",
|
||||
product_name="Test Product",
|
||||
quantity_sold=5,
|
||||
unit_price=Decimal("2.00"),
|
||||
revenue=Decimal("15.00"), # Should be 10.00 (5 * 2.00)
|
||||
discount_applied=Decimal("0")
|
||||
)
|
||||
|
||||
# This should not raise an error, just log a warning
|
||||
await sales_service._validate_sales_data(sales_data, sample_tenant_id)
|
||||
|
||||
async def test_post_create_actions(self, sales_service):
|
||||
"""Test post-create actions"""
|
||||
mock_record = AsyncMock()
|
||||
mock_record.id = uuid4()
|
||||
|
||||
# Should not raise any exceptions
|
||||
await sales_service._post_create_actions(mock_record)
|
||||
Reference in New Issue
Block a user