test: add tests for merchant dashboard metrics and fix invoice template location
Move invoice PDF template from app/templates/invoices/ to app/modules/orders/templates/invoices/ where InvoicePDFService expects it. Expand invoice PDF tests to validate template path and existence. Add unit tests for get_merchant_metrics() in tenancy, billing, and customer metrics providers. Add unit tests for StatsAggregatorService merchant methods. Add integration tests for the merchant dashboard stats endpoint. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
0
app/modules/core/tests/integration/__init__.py
Normal file
0
app/modules/core/tests/integration/__init__.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# app/modules/core/tests/integration/test_merchant_dashboard_routes.py
|
||||
"""
|
||||
Integration tests for merchant dashboard API routes.
|
||||
|
||||
Tests the merchant dashboard endpoint at:
|
||||
GET /api/v1/merchants/core/dashboard/stats
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.deps import get_current_merchant_api, get_merchant_for_current_user
|
||||
from app.modules.billing.models import (
|
||||
MerchantSubscription,
|
||||
SubscriptionStatus,
|
||||
SubscriptionTier,
|
||||
)
|
||||
from app.modules.customers.models.customer import Customer
|
||||
from app.modules.tenancy.models import Merchant, Platform, Store, StoreUser, User
|
||||
from main import app
|
||||
from models.schema.auth import UserContext
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
BASE = "/api/v1/merchants/core"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_owner(db):
|
||||
"""Create a merchant owner user."""
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
auth = AuthManager()
|
||||
user = User(
|
||||
email=f"dashowner_{uuid.uuid4().hex[:8]}@test.com",
|
||||
username=f"dashowner_{uuid.uuid4().hex[:8]}",
|
||||
hashed_password=auth.hash_password("pass123"),
|
||||
role="merchant_owner",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
return user
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_platform(db):
|
||||
"""Create a platform."""
|
||||
platform = Platform(
|
||||
code=f"dp_{uuid.uuid4().hex[:8]}",
|
||||
name="Dashboard Test Platform",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(platform)
|
||||
db.commit()
|
||||
db.refresh(platform)
|
||||
return platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_merchant(db, dash_owner):
|
||||
"""Create a merchant."""
|
||||
merchant = Merchant(
|
||||
name="Dashboard Test Merchant",
|
||||
owner_user_id=dash_owner.id,
|
||||
contact_email=dash_owner.email,
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(merchant)
|
||||
db.commit()
|
||||
db.refresh(merchant)
|
||||
return merchant
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_stores(db, dash_merchant):
|
||||
"""Create 3 stores (2 active, 1 inactive)."""
|
||||
stores = []
|
||||
for i, active in enumerate([True, True, False]):
|
||||
uid = uuid.uuid4().hex[:8].upper()
|
||||
store = Store(
|
||||
merchant_id=dash_merchant.id,
|
||||
store_code=f"DSTORE_{uid}",
|
||||
subdomain=f"dstore{uid.lower()}",
|
||||
name=f"Dashboard Store {i}",
|
||||
is_active=active,
|
||||
is_verified=True,
|
||||
)
|
||||
db.add(store)
|
||||
stores.append(store)
|
||||
db.commit()
|
||||
for s in stores:
|
||||
db.refresh(s)
|
||||
return stores
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_team_members(db, dash_stores, dash_owner):
|
||||
"""Create team members across stores."""
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
auth = AuthManager()
|
||||
users = []
|
||||
for _ in range(2):
|
||||
u = User(
|
||||
email=f"dteam_{uuid.uuid4().hex[:8]}@test.com",
|
||||
username=f"dteam_{uuid.uuid4().hex[:8]}",
|
||||
hashed_password=auth.hash_password("pass123"),
|
||||
role="store_user",
|
||||
is_active=True,
|
||||
)
|
||||
db.add(u)
|
||||
users.append(u)
|
||||
db.flush()
|
||||
|
||||
db.add(StoreUser(store_id=dash_stores[0].id, user_id=users[0].id, is_active=True))
|
||||
db.add(StoreUser(store_id=dash_stores[1].id, user_id=users[1].id, is_active=True))
|
||||
db.commit()
|
||||
return users
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_customers(db, dash_stores):
|
||||
"""Create customers in the merchant's stores."""
|
||||
for i in range(4):
|
||||
uid = uuid.uuid4().hex[:8]
|
||||
db.add(
|
||||
Customer(
|
||||
store_id=dash_stores[0].id,
|
||||
email=f"dc_{uid}@test.com",
|
||||
hashed_password="hashed", # noqa: SEC001
|
||||
first_name=f"F{i}",
|
||||
last_name=f"L{i}",
|
||||
customer_number=f"DC{uid}",
|
||||
is_active=True,
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_subscription(db, dash_merchant, dash_platform):
|
||||
"""Create an active subscription."""
|
||||
tier = SubscriptionTier(
|
||||
code=f"pro_{uuid.uuid4().hex[:8]}",
|
||||
name="Professional",
|
||||
description="Pro",
|
||||
price_monthly_cents=2900,
|
||||
price_annual_cents=29000,
|
||||
display_order=1,
|
||||
is_active=True,
|
||||
is_public=True,
|
||||
platform_id=dash_platform.id,
|
||||
)
|
||||
db.add(tier)
|
||||
db.flush()
|
||||
|
||||
sub = MerchantSubscription(
|
||||
merchant_id=dash_merchant.id,
|
||||
platform_id=dash_platform.id,
|
||||
tier_id=tier.id,
|
||||
status=SubscriptionStatus.ACTIVE.value,
|
||||
is_annual=False,
|
||||
period_start=datetime.now(UTC),
|
||||
period_end=datetime.now(UTC) + timedelta(days=30),
|
||||
)
|
||||
db.add(sub)
|
||||
db.commit()
|
||||
db.refresh(sub)
|
||||
return sub
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dash_auth(dash_owner, dash_merchant):
|
||||
"""Override auth dependencies for dashboard merchant."""
|
||||
user_context = UserContext(
|
||||
id=dash_owner.id,
|
||||
email=dash_owner.email,
|
||||
username=dash_owner.username,
|
||||
role="merchant_owner",
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
def _override_merchant():
|
||||
return dash_merchant
|
||||
|
||||
def _override_user():
|
||||
return user_context
|
||||
|
||||
app.dependency_overrides[get_merchant_for_current_user] = _override_merchant
|
||||
app.dependency_overrides[get_current_merchant_api] = _override_user
|
||||
yield {"Authorization": "Bearer fake-token"}
|
||||
app.dependency_overrides.pop(get_merchant_for_current_user, None)
|
||||
app.dependency_overrides.pop(get_current_merchant_api, None)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Tests
|
||||
# ============================================================================
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.core
|
||||
class TestMerchantDashboardStats:
|
||||
"""Tests for GET /api/v1/merchants/core/dashboard/stats."""
|
||||
|
||||
def test_returns_correct_stats(
|
||||
self, client, db, dash_auth, dash_stores, dash_team_members, dash_customers, dash_subscription
|
||||
):
|
||||
"""Endpoint returns correct aggregated stats."""
|
||||
response = client.get(f"{BASE}/dashboard/stats", headers=dash_auth)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total_stores"] == 3
|
||||
assert data["active_subscriptions"] == 1
|
||||
assert data["total_customers"] == 4
|
||||
assert data["team_members"] == 2
|
||||
|
||||
def test_returns_zeros_when_empty(self, client, db, dash_auth):
|
||||
"""Returns zero counts when merchant has no data."""
|
||||
response = client.get(f"{BASE}/dashboard/stats", headers=dash_auth)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total_stores"] == 0
|
||||
assert data["active_subscriptions"] == 0
|
||||
assert data["total_customers"] == 0
|
||||
assert data["team_members"] == 0
|
||||
|
||||
def test_requires_auth(self, client):
|
||||
"""Returns 401 without auth."""
|
||||
# Remove any overrides
|
||||
app.dependency_overrides.pop(get_merchant_for_current_user, None)
|
||||
app.dependency_overrides.pop(get_current_merchant_api, None)
|
||||
response = client.get(f"{BASE}/dashboard/stats")
|
||||
assert response.status_code == 401
|
||||
Reference in New Issue
Block a user