2025-08-12 18:17:30 +02:00
|
|
|
# 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()
|
2025-08-14 16:47:34 +02:00
|
|
|
mock_record.inventory_product_id = sample_sales_data.inventory_product_id
|
2025-08-12 18:17:30 +02:00
|
|
|
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
|
2025-08-14 16:47:34 +02:00
|
|
|
inventory_product_id="550e8400-e29b-41d4-a716-446655440000",
|
2025-08-12 18:17:30 +02:00
|
|
|
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(
|
2025-08-14 16:47:34 +02:00
|
|
|
inventory_product_id="550e8400-e29b-41d4-a716-446655440999",
|
2025-08-12 18:17:30 +02:00
|
|
|
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()
|
2025-08-14 16:47:34 +02:00
|
|
|
mock_updated.inventory_product_id = "550e8400-e29b-41d4-a716-446655440999"
|
2025-08-12 18:17:30 +02:00
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
|
2025-08-14 16:47:34 +02:00
|
|
|
assert result.inventory_product_id == "550e8400-e29b-41d4-a716-446655440999"
|
2025-08-12 18:17:30 +02:00
|
|
|
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()
|
2025-08-14 16:47:34 +02:00
|
|
|
update_data = SalesDataUpdate(inventory_product_id="550e8400-e29b-41d4-a716-446655440999", product_name="Updated Product")
|
2025-08-12 18:17:30 +02:00
|
|
|
|
|
|
|
|
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"""
|
2025-08-14 16:47:34 +02:00
|
|
|
inventory_product_id = "550e8400-e29b-41d4-a716-446655440000"
|
2025-08-12 18:17:30 +02:00
|
|
|
|
|
|
|
|
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):
|
2025-08-14 16:47:34 +02:00
|
|
|
result = await sales_service.get_product_sales(sample_tenant_id, inventory_product_id)
|
2025-08-12 18:17:30 +02:00
|
|
|
|
|
|
|
|
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),
|
2025-08-14 16:47:34 +02:00
|
|
|
inventory_product_id="550e8400-e29b-41d4-a716-446655440000",
|
2025-08-12 18:17:30 +02:00
|
|
|
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)
|