417 lines
15 KiB
Python
417 lines
15 KiB
Python
|
|
# 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
|