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