Files
orion/tests/unit/services/test_admin_customer_service.py
Samir Boulahtit 228163d920 feat(arch): add API-007 rule to enforce layered architecture
Add architecture rule that detects when API routes import database
models directly, enforcing Routes → Services → Models pattern.

Changes:
- Add API-007 rule to .architecture-rules/api.yaml
- Add _check_no_model_imports() validation to validator script
- Update customer imports to use canonical module location
- Add storefront module restructure implementation plan

The validator now detects 81 violations across 67 API files where
database models are imported directly instead of going through
services. This is Phase 1 of the storefront restructure plan.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 22:23:00 +01:00

281 lines
9.4 KiB
Python

# tests/unit/services/test_admin_customer_service.py
"""
Unit tests for AdminCustomerService.
"""
from decimal import Decimal
import pytest
from app.exceptions.customer import CustomerNotFoundException
from app.services.admin_customer_service import AdminCustomerService
from app.modules.customers.models.customer import Customer
@pytest.fixture
def admin_customer_service():
"""Create AdminCustomerService instance."""
return AdminCustomerService()
@pytest.fixture
def customer_with_orders(db, test_vendor, test_customer):
"""Create a customer with order data."""
test_customer.total_orders = 5
test_customer.total_spent = Decimal("250.00")
db.commit()
db.refresh(test_customer)
return test_customer
@pytest.fixture
def multiple_customers(db, test_vendor):
"""Create multiple customers for testing."""
customers = []
for i in range(5):
customer = Customer(
vendor_id=test_vendor.id,
email=f"customer{i}@example.com",
hashed_password="hashed_password_placeholder",
first_name=f"First{i}",
last_name=f"Last{i}",
customer_number=f"CUST-00{i}",
is_active=(i % 2 == 0), # Alternate active/inactive
total_orders=i,
total_spent=Decimal(str(i * 100)),
)
db.add(customer)
customers.append(customer)
db.commit()
for c in customers:
db.refresh(c)
return customers
@pytest.mark.unit
class TestAdminCustomerServiceList:
"""Tests for list_customers method."""
def test_list_customers_empty(self, db, admin_customer_service, test_vendor):
"""Test listing customers when none exist."""
customers, total = admin_customer_service.list_customers(db)
assert customers == []
assert total == 0
def test_list_customers_basic(self, db, admin_customer_service, test_customer):
"""Test basic customer listing."""
customers, total = admin_customer_service.list_customers(db)
assert total == 1
assert len(customers) == 1
assert customers[0]["id"] == test_customer.id
assert customers[0]["email"] == test_customer.email
def test_list_customers_with_vendor_info(
self, db, admin_customer_service, test_customer, test_vendor
):
"""Test that vendor info is included."""
customers, total = admin_customer_service.list_customers(db)
assert customers[0]["vendor_name"] == test_vendor.name
assert customers[0]["vendor_code"] == test_vendor.vendor_code
def test_list_customers_filter_by_vendor(
self, db, admin_customer_service, multiple_customers, test_vendor
):
"""Test filtering by vendor ID."""
customers, total = admin_customer_service.list_customers(
db, vendor_id=test_vendor.id
)
assert total == 5
for customer in customers:
assert customer["vendor_id"] == test_vendor.id
def test_list_customers_filter_by_active_status(
self, db, admin_customer_service, multiple_customers
):
"""Test filtering by active status."""
# Get active customers (0, 2, 4 = 3 customers)
customers, total = admin_customer_service.list_customers(db, is_active=True)
assert total == 3
# Get inactive customers (1, 3 = 2 customers)
customers, total = admin_customer_service.list_customers(db, is_active=False)
assert total == 2
def test_list_customers_search_by_email(
self, db, admin_customer_service, multiple_customers
):
"""Test searching by email."""
customers, total = admin_customer_service.list_customers(
db, search="customer2@"
)
assert total == 1
assert customers[0]["email"] == "customer2@example.com"
def test_list_customers_search_by_name(
self, db, admin_customer_service, multiple_customers
):
"""Test searching by name."""
customers, total = admin_customer_service.list_customers(db, search="First3")
assert total == 1
assert customers[0]["first_name"] == "First3"
def test_list_customers_search_by_customer_number(
self, db, admin_customer_service, multiple_customers
):
"""Test searching by customer number."""
customers, total = admin_customer_service.list_customers(db, search="CUST-001")
assert total == 1
assert customers[0]["customer_number"] == "CUST-001"
def test_list_customers_pagination(
self, db, admin_customer_service, multiple_customers
):
"""Test pagination."""
# Get first page
customers, total = admin_customer_service.list_customers(db, skip=0, limit=2)
assert len(customers) == 2
assert total == 5
# Get second page
customers, total = admin_customer_service.list_customers(db, skip=2, limit=2)
assert len(customers) == 2
# Get last page
customers, total = admin_customer_service.list_customers(db, skip=4, limit=2)
assert len(customers) == 1
@pytest.mark.unit
class TestAdminCustomerServiceStats:
"""Tests for get_customer_stats method."""
def test_get_customer_stats_empty(self, db, admin_customer_service, test_vendor):
"""Test stats when no customers exist."""
stats = admin_customer_service.get_customer_stats(db)
assert stats["total"] == 0
assert stats["active"] == 0
assert stats["inactive"] == 0
assert stats["with_orders"] == 0
assert stats["total_spent"] == 0
assert stats["total_orders"] == 0
assert stats["avg_order_value"] == 0
def test_get_customer_stats_with_data(
self, db, admin_customer_service, multiple_customers
):
"""Test stats with customer data."""
stats = admin_customer_service.get_customer_stats(db)
assert stats["total"] == 5
assert stats["active"] == 3 # 0, 2, 4
assert stats["inactive"] == 2 # 1, 3
# with_orders = customers with total_orders > 0 (1, 2, 3, 4 = 4 customers)
assert stats["with_orders"] == 4
# total_spent = 0 + 100 + 200 + 300 + 400 = 1000
assert stats["total_spent"] == 1000.0
# total_orders = 0 + 1 + 2 + 3 + 4 = 10
assert stats["total_orders"] == 10
def test_get_customer_stats_by_vendor(
self, db, admin_customer_service, test_customer, test_vendor
):
"""Test stats filtered by vendor."""
stats = admin_customer_service.get_customer_stats(db, vendor_id=test_vendor.id)
assert stats["total"] == 1
def test_get_customer_stats_avg_order_value(
self, db, admin_customer_service, customer_with_orders
):
"""Test average order value calculation."""
stats = admin_customer_service.get_customer_stats(db)
# total_spent = 250, total_orders = 5
# avg = 250 / 5 = 50
assert stats["avg_order_value"] == 50.0
@pytest.mark.unit
class TestAdminCustomerServiceGetCustomer:
"""Tests for get_customer method."""
def test_get_customer_success(self, db, admin_customer_service, test_customer):
"""Test getting customer by ID."""
customer = admin_customer_service.get_customer(db, test_customer.id)
assert customer["id"] == test_customer.id
assert customer["email"] == test_customer.email
assert customer["first_name"] == test_customer.first_name
assert customer["last_name"] == test_customer.last_name
def test_get_customer_with_vendor_info(
self, db, admin_customer_service, test_customer, test_vendor
):
"""Test vendor info in customer detail."""
customer = admin_customer_service.get_customer(db, test_customer.id)
assert customer["vendor_name"] == test_vendor.name
assert customer["vendor_code"] == test_vendor.vendor_code
def test_get_customer_not_found(self, db, admin_customer_service):
"""Test error when customer not found."""
with pytest.raises(CustomerNotFoundException):
admin_customer_service.get_customer(db, 99999)
@pytest.mark.unit
class TestAdminCustomerServiceToggleStatus:
"""Tests for toggle_customer_status method."""
def test_toggle_status_activate(
self, db, admin_customer_service, test_customer, test_admin
):
"""Test activating an inactive customer."""
# Make customer inactive first
test_customer.is_active = False
db.commit()
result = admin_customer_service.toggle_customer_status(
db, test_customer.id, test_admin.email
)
db.commit()
assert result["id"] == test_customer.id
assert result["is_active"] is True
assert "activated" in result["message"]
def test_toggle_status_deactivate(
self, db, admin_customer_service, test_customer, test_admin
):
"""Test deactivating an active customer."""
test_customer.is_active = True
db.commit()
result = admin_customer_service.toggle_customer_status(
db, test_customer.id, test_admin.email
)
db.commit()
assert result["id"] == test_customer.id
assert result["is_active"] is False
assert "deactivated" in result["message"]
def test_toggle_status_not_found(
self, db, admin_customer_service, test_admin
):
"""Test error when customer not found."""
with pytest.raises(CustomerNotFoundException):
admin_customer_service.toggle_customer_status(
db, 99999, test_admin.email
)