Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View 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

View 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

View 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

View 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"]

View 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

View 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

View 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)