Initial commit - production deployment
This commit is contained in:
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