The store and merchant init-alpine.js derive currentPage from the URL's last segment (e.g., /loyalty/program -> 'program'). Loyalty menu items used prefixed IDs like 'loyalty-program' which never matched, so sidebar items never highlighted. Fixed by renaming all store/merchant menu item IDs and JS currentPage values to match URL segments: program, cards, analytics, transactions, pins, settings — consistent with how every other module works. Also reverted the init-alpine.js guard that broke storeCode extraction, and added missing loyalty.common.contact_admin_setup translation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
352 lines
13 KiB
Python
352 lines
13 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 router
|
|
|
|
return router
|
|
|
|
|
|
def _get_merchant_router():
|
|
"""Lazy import of merchant router to avoid circular imports."""
|
|
from app.modules.loyalty.routes.api.merchant import router as merchant_router
|
|
|
|
return merchant_router
|
|
|
|
|
|
def _get_store_router():
|
|
"""Lazy import of store router to avoid circular imports."""
|
|
from app.modules.loyalty.routes.api.store import router
|
|
|
|
return 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
|
|
|
|
|
|
def _get_onboarding_provider():
|
|
"""Lazy import of onboarding provider to avoid circular imports."""
|
|
from app.modules.loyalty.services.loyalty_onboarding_service import (
|
|
loyalty_onboarding_provider,
|
|
)
|
|
|
|
return loyalty_onboarding_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
|
|
"wallet-debug", # Wallet diagnostics (super admin)
|
|
],
|
|
FrontendType.STORE: [
|
|
"terminal", # Loyalty terminal
|
|
"cards", # Customer cards
|
|
"pins", # Staff PINs
|
|
"program", # Program config
|
|
"analytics", # Store analytics
|
|
],
|
|
FrontendType.MERCHANT: [
|
|
"program", # Merchant loyalty program
|
|
"cards", # Customer cards
|
|
"analytics", # Merchant loyalty analytics
|
|
"transactions", # Transaction feed
|
|
"pins", # Staff PINs
|
|
"settings", # Settings (read-only)
|
|
],
|
|
},
|
|
# 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,
|
|
),
|
|
MenuItemDefinition(
|
|
id="wallet-debug",
|
|
label_key="loyalty.menu.wallet_debug",
|
|
icon="beaker",
|
|
route="/admin/loyalty/wallet-debug",
|
|
order=30,
|
|
is_super_admin_only=True,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
FrontendType.STORE: [
|
|
MenuSectionDefinition(
|
|
id="loyalty",
|
|
label_key="loyalty.menu.loyalty_programs",
|
|
icon="gift",
|
|
order=35,
|
|
items=[
|
|
MenuItemDefinition(
|
|
id="terminal",
|
|
label_key="loyalty.menu.terminal",
|
|
icon="gift",
|
|
route="/store/{store_code}/loyalty/terminal",
|
|
order=10,
|
|
requires_permission="loyalty.view_programs",
|
|
),
|
|
MenuItemDefinition(
|
|
id="cards",
|
|
label_key="loyalty.menu.customer_cards",
|
|
icon="identification",
|
|
route="/store/{store_code}/loyalty/cards",
|
|
order=20,
|
|
requires_permission="loyalty.view_programs",
|
|
),
|
|
MenuItemDefinition(
|
|
id="pins",
|
|
label_key="loyalty.menu.staff_pins",
|
|
icon="key",
|
|
route="/store/{store_code}/loyalty/pins",
|
|
order=22,
|
|
requires_permission="loyalty.view_programs",
|
|
),
|
|
MenuItemDefinition(
|
|
id="program",
|
|
label_key="loyalty.menu.program",
|
|
icon="cog",
|
|
route="/store/{store_code}/loyalty/program",
|
|
order=25,
|
|
requires_permission="loyalty.view_programs",
|
|
),
|
|
MenuItemDefinition(
|
|
id="analytics",
|
|
label_key="loyalty.menu.analytics",
|
|
icon="chart-bar",
|
|
route="/store/{store_code}/loyalty/analytics",
|
|
order=30,
|
|
requires_permission="loyalty.view_programs",
|
|
),
|
|
],
|
|
),
|
|
],
|
|
FrontendType.MERCHANT: [
|
|
MenuSectionDefinition(
|
|
id="loyalty",
|
|
label_key="loyalty.menu.loyalty",
|
|
icon="gift",
|
|
order=60,
|
|
items=[
|
|
MenuItemDefinition(
|
|
id="program",
|
|
label_key="loyalty.menu.program",
|
|
icon="gift",
|
|
route="/merchants/loyalty/program",
|
|
order=10,
|
|
),
|
|
MenuItemDefinition(
|
|
id="cards",
|
|
label_key="loyalty.menu.customer_cards",
|
|
icon="identification",
|
|
route="/merchants/loyalty/cards",
|
|
order=15,
|
|
),
|
|
MenuItemDefinition(
|
|
id="analytics",
|
|
label_key="loyalty.menu.analytics",
|
|
icon="chart-bar",
|
|
route="/merchants/loyalty/analytics",
|
|
order=20,
|
|
),
|
|
MenuItemDefinition(
|
|
id="transactions",
|
|
label_key="loyalty.menu.transactions",
|
|
icon="clock",
|
|
route="/merchants/loyalty/transactions",
|
|
order=25,
|
|
),
|
|
MenuItemDefinition(
|
|
id="pins",
|
|
label_key="loyalty.menu.staff_pins",
|
|
icon="key",
|
|
route="/merchants/loyalty/pins",
|
|
order=30,
|
|
),
|
|
MenuItemDefinition(
|
|
id="settings",
|
|
label_key="loyalty.menu.settings",
|
|
icon="cog",
|
|
route="/merchants/loyalty/settings",
|
|
order=35,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
FrontendType.STOREFRONT: [
|
|
MenuSectionDefinition(
|
|
id="account",
|
|
label_key=None,
|
|
order=10,
|
|
items=[
|
|
MenuItemDefinition(
|
|
id="loyalty",
|
|
label_key="storefront.account.loyalty",
|
|
icon="gift",
|
|
route="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,
|
|
# Onboarding provider for post-signup checklist
|
|
onboarding_provider=_get_onboarding_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.merchant_router = _get_merchant_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"]
|