- Add LoyaltyFeatureProvider with 11 BINARY/MERCHANT features for billing feature gating, wired into loyalty module definition - Fix subscription-tiers admin page showing 0 features by populating feature_codes from tier relationship in all admin tier endpoints - Fix merchants admin page showing 0 stores and N/A owner by adding store_count and owner_email to MerchantResponse and eager-loading owner - Add "no tiers" warning with link in subscription creation modal when platform has no configured tiers - Add missing mobile menu panel to storefront base template so hamburger toggle actually shows navigation links Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
243 lines
8.3 KiB
Python
243 lines
8.3 KiB
Python
# app/modules/loyalty/definition.py
|
|
"""
|
|
Loyalty module definition.
|
|
|
|
Defines the loyalty module including its features, menu items,
|
|
route configurations, and scheduled tasks.
|
|
"""
|
|
|
|
from app.modules.base import (
|
|
MenuItemDefinition,
|
|
MenuSectionDefinition,
|
|
ModuleDefinition,
|
|
PermissionDefinition,
|
|
ScheduledTask,
|
|
)
|
|
from app.modules.enums import FrontendType
|
|
|
|
|
|
def _get_admin_router():
|
|
"""Lazy import of admin router to avoid circular imports."""
|
|
from app.modules.loyalty.routes.api.admin import admin_router
|
|
|
|
return admin_router
|
|
|
|
|
|
def _get_store_router():
|
|
"""Lazy import of store router to avoid circular imports."""
|
|
from app.modules.loyalty.routes.api.store import store_router
|
|
|
|
return store_router
|
|
|
|
|
|
def _get_platform_router():
|
|
"""Lazy import of platform router to avoid circular imports."""
|
|
from app.modules.loyalty.routes.api.platform import platform_router
|
|
|
|
return platform_router
|
|
|
|
|
|
def _get_storefront_router():
|
|
"""Lazy import of storefront router to avoid circular imports."""
|
|
from app.modules.loyalty.routes.api.storefront import storefront_router
|
|
|
|
return storefront_router
|
|
|
|
|
|
def _get_feature_provider():
|
|
"""Lazy import of feature provider to avoid circular imports."""
|
|
from app.modules.loyalty.services.loyalty_features import loyalty_feature_provider
|
|
|
|
return loyalty_feature_provider
|
|
|
|
|
|
# Loyalty module definition
|
|
loyalty_module = ModuleDefinition(
|
|
code="loyalty",
|
|
name="Loyalty Programs",
|
|
description=(
|
|
"Stamp-based and points-based loyalty programs with Google Wallet "
|
|
"and Apple Wallet integration. Includes anti-fraud features like "
|
|
"staff PINs, cooldown periods, and daily limits."
|
|
),
|
|
version="1.0.0",
|
|
requires=["customers"], # Depends on customers module for customer data
|
|
features=[
|
|
# Core features
|
|
"loyalty_stamps", # Stamp-based loyalty
|
|
"loyalty_points", # Points-based loyalty
|
|
"loyalty_hybrid", # Both stamps and points
|
|
# Card management
|
|
"loyalty_cards", # Customer card management
|
|
"loyalty_enrollment", # Customer enrollment
|
|
# Staff/fraud prevention
|
|
"loyalty_staff_pins", # Staff PIN management
|
|
"loyalty_anti_fraud", # Cooldown, daily limits
|
|
# Wallet integration
|
|
"loyalty_google_wallet", # Google Wallet passes
|
|
"loyalty_apple_wallet", # Apple Wallet passes
|
|
# Analytics
|
|
"loyalty_stats", # Dashboard statistics
|
|
"loyalty_reports", # Transaction reports
|
|
],
|
|
# Module-driven permissions
|
|
permissions=[
|
|
PermissionDefinition(
|
|
id="loyalty.view_programs",
|
|
label_key="loyalty.permissions.view_programs",
|
|
description_key="loyalty.permissions.view_programs_desc",
|
|
category="loyalty",
|
|
),
|
|
PermissionDefinition(
|
|
id="loyalty.manage_programs",
|
|
label_key="loyalty.permissions.manage_programs",
|
|
description_key="loyalty.permissions.manage_programs_desc",
|
|
category="loyalty",
|
|
),
|
|
PermissionDefinition(
|
|
id="loyalty.view_rewards",
|
|
label_key="loyalty.permissions.view_rewards",
|
|
description_key="loyalty.permissions.view_rewards_desc",
|
|
category="loyalty",
|
|
),
|
|
PermissionDefinition(
|
|
id="loyalty.manage_rewards",
|
|
label_key="loyalty.permissions.manage_rewards",
|
|
description_key="loyalty.permissions.manage_rewards_desc",
|
|
category="loyalty",
|
|
),
|
|
],
|
|
menu_items={
|
|
FrontendType.ADMIN: [
|
|
"loyalty-programs", # View all programs
|
|
"loyalty-analytics", # Platform-wide stats
|
|
],
|
|
FrontendType.STORE: [
|
|
"loyalty", # Loyalty dashboard
|
|
"loyalty-cards", # Customer cards
|
|
"loyalty-stats", # Store stats
|
|
],
|
|
},
|
|
# New module-driven menu definitions
|
|
menus={
|
|
FrontendType.ADMIN: [
|
|
MenuSectionDefinition(
|
|
id="loyalty",
|
|
label_key="loyalty.menu.loyalty",
|
|
icon="gift",
|
|
order=55,
|
|
items=[
|
|
MenuItemDefinition(
|
|
id="loyalty-programs",
|
|
label_key="loyalty.menu.programs",
|
|
icon="gift",
|
|
route="/admin/loyalty/programs",
|
|
order=10,
|
|
),
|
|
MenuItemDefinition(
|
|
id="loyalty-analytics",
|
|
label_key="loyalty.menu.analytics",
|
|
icon="chart-bar",
|
|
route="/admin/loyalty/analytics",
|
|
order=20,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
FrontendType.STORE: [
|
|
MenuSectionDefinition(
|
|
id="loyalty",
|
|
label_key="loyalty.menu.loyalty_programs",
|
|
icon="gift",
|
|
order=35,
|
|
items=[
|
|
MenuItemDefinition(
|
|
id="loyalty",
|
|
label_key="loyalty.menu.dashboard",
|
|
icon="gift",
|
|
route="/store/{store_code}/loyalty",
|
|
order=10,
|
|
),
|
|
MenuItemDefinition(
|
|
id="loyalty-cards",
|
|
label_key="loyalty.menu.customer_cards",
|
|
icon="identification",
|
|
route="/store/{store_code}/loyalty/cards",
|
|
order=20,
|
|
),
|
|
MenuItemDefinition(
|
|
id="loyalty-stats",
|
|
label_key="loyalty.menu.statistics",
|
|
icon="chart-bar",
|
|
route="/store/{store_code}/loyalty/stats",
|
|
order=30,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
FrontendType.STOREFRONT: [
|
|
MenuSectionDefinition(
|
|
id="account",
|
|
label_key=None,
|
|
order=10,
|
|
items=[
|
|
MenuItemDefinition(
|
|
id="loyalty",
|
|
label_key="storefront.account.loyalty",
|
|
icon="gift",
|
|
route="storefront/account/loyalty",
|
|
order=60,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
},
|
|
is_core=False, # Loyalty can be disabled
|
|
# =========================================================================
|
|
# Self-Contained Module Configuration
|
|
# =========================================================================
|
|
is_self_contained=True,
|
|
services_path="app.modules.loyalty.services",
|
|
models_path="app.modules.loyalty.models",
|
|
schemas_path="app.modules.loyalty.schemas",
|
|
exceptions_path="app.modules.loyalty.exceptions",
|
|
tasks_path="app.modules.loyalty.tasks",
|
|
migrations_path="migrations",
|
|
# =========================================================================
|
|
# Scheduled Tasks
|
|
# =========================================================================
|
|
scheduled_tasks=[
|
|
ScheduledTask(
|
|
name="loyalty.sync_wallet_passes",
|
|
task="app.modules.loyalty.tasks.wallet_sync.sync_wallet_passes",
|
|
schedule="0 * * * *", # Hourly
|
|
options={"queue": "scheduled"},
|
|
),
|
|
ScheduledTask(
|
|
name="loyalty.expire_points",
|
|
task="app.modules.loyalty.tasks.point_expiration.expire_points",
|
|
schedule="0 2 * * *", # Daily at 02:00
|
|
options={"queue": "scheduled"},
|
|
),
|
|
],
|
|
# Feature provider for billing feature gating
|
|
feature_provider=_get_feature_provider,
|
|
)
|
|
|
|
|
|
def get_loyalty_module_with_routers() -> ModuleDefinition:
|
|
"""
|
|
Get loyalty module with routers attached.
|
|
|
|
This function attaches the routers lazily to avoid circular imports
|
|
during module initialization.
|
|
"""
|
|
loyalty_module.admin_router = _get_admin_router()
|
|
loyalty_module.store_router = _get_store_router()
|
|
loyalty_module.platform_router = _get_platform_router()
|
|
loyalty_module.storefront_router = _get_storefront_router()
|
|
return loyalty_module
|
|
|
|
|
|
__all__ = ["loyalty_module", "get_loyalty_module_with_routers"]
|