Files
orion/app/modules/loyalty/definition.py
Samir Boulahtit f89c0382f0
Some checks failed
CI / ruff (push) Successful in 12s
CI / validate (push) Successful in 27s
CI / dependency-scanning (push) Successful in 32s
CI / pytest (push) Failing after 1h13m39s
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
feat(loyalty): wallet debug page, Google Wallet fixes, and module config env_file standardization
- Add wallet diagnostics page at /admin/loyalty/wallet-debug (super admin only)
  with explorer-sidebar pattern: config validation, class status, card inspector,
  save URL tester, recent enrollments, and Apple Wallet status panels
- Fix Google Wallet fat JWT: include both loyaltyClasses and loyaltyObjects in
  payload, use UNDER_REVIEW instead of DRAFT for class reviewStatus
- Fix StorefrontProgramResponse schema: accept google_class_id values while
  keeping exclude=True (was rejecting non-None values)
- Standardize all module configs to read from .env file directly
  (env_file=".env", extra="ignore") matching core Settings pattern
- Add MOD-026 architecture rule enforcing env_file in module configs
- Add SVC-005 noqa support in architecture validator
- Add test files for dev_tools domain_health and isolation_audit services
- Add google_wallet_status.py script for querying Google Wallet API
- Use table_wrapper macro in wallet-debug.html (FE-005 compliance)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 22:18:39 +01:00

311 lines
11 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
"loyalty-program", # Program config
"loyalty-analytics", # Store analytics
],
FrontendType.MERCHANT: [
"loyalty-program", # Merchant loyalty program
"loyalty-analytics", # Merchant loyalty analytics
],
},
# 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="loyalty-program",
label_key="loyalty.menu.program",
icon="cog",
route="/store/{store_code}/loyalty/program",
order=25,
requires_permission="loyalty.view_programs",
),
MenuItemDefinition(
id="loyalty-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="loyalty-program",
label_key="loyalty.menu.program",
icon="gift",
route="/merchants/loyalty/program",
order=10,
),
MenuItemDefinition(
id="loyalty-analytics",
label_key="loyalty.menu.analytics",
icon="chart-bar",
route="/merchants/loyalty/analytics",
order=20,
),
],
),
],
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"]