Fixes deployment test failures where get_store_usage() and get_merchant_usage() were called with db=None but attempted to run queries. Also adds noqa suppressions for pre-existing security validator findings in dev-toolbar (innerHTML with trusted content) and test fixtures (hardcoded test passwords). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
269 lines
8.9 KiB
Python
269 lines
8.9 KiB
Python
# app/modules/loyalty/services/loyalty_features.py
|
|
"""
|
|
Loyalty feature provider for the billing feature system.
|
|
|
|
Declares loyalty-related billable features (stamps, points, cards, wallets, etc.)
|
|
for feature gating.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
from app.modules.contracts.features import (
|
|
FeatureDeclaration,
|
|
FeatureScope,
|
|
FeatureType,
|
|
FeatureUsage,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.orm import Session
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class LoyaltyFeatureProvider:
|
|
"""Feature provider for the loyalty module.
|
|
|
|
Declares:
|
|
- loyalty_stamps: stamp-based loyalty programs
|
|
- loyalty_points: points-based loyalty programs
|
|
- loyalty_hybrid: combined stamps and points
|
|
- loyalty_cards: customer card management
|
|
- loyalty_enrollment: customer enrollment
|
|
- loyalty_staff_pins: staff PIN management
|
|
- loyalty_anti_fraud: cooldown and daily limits
|
|
- loyalty_google_wallet: Google Wallet passes
|
|
- loyalty_apple_wallet: Apple Wallet passes
|
|
- loyalty_stats: dashboard statistics
|
|
- loyalty_reports: transaction reports
|
|
"""
|
|
|
|
@property
|
|
def feature_category(self) -> str:
|
|
return "loyalty"
|
|
|
|
def get_feature_declarations(self) -> list[FeatureDeclaration]:
|
|
return [
|
|
FeatureDeclaration(
|
|
code="loyalty_stamps",
|
|
name_key="loyalty.features.loyalty_stamps.name",
|
|
description_key="loyalty.features.loyalty_stamps.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="stamp",
|
|
display_order=10,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_points",
|
|
name_key="loyalty.features.loyalty_points.name",
|
|
description_key="loyalty.features.loyalty_points.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="coins",
|
|
display_order=20,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_hybrid",
|
|
name_key="loyalty.features.loyalty_hybrid.name",
|
|
description_key="loyalty.features.loyalty_hybrid.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="layers",
|
|
display_order=30,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_cards",
|
|
name_key="loyalty.features.loyalty_cards.name",
|
|
description_key="loyalty.features.loyalty_cards.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="credit-card",
|
|
display_order=40,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_enrollment",
|
|
name_key="loyalty.features.loyalty_enrollment.name",
|
|
description_key="loyalty.features.loyalty_enrollment.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="user-plus",
|
|
display_order=50,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_staff_pins",
|
|
name_key="loyalty.features.loyalty_staff_pins.name",
|
|
description_key="loyalty.features.loyalty_staff_pins.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="shield",
|
|
display_order=60,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_anti_fraud",
|
|
name_key="loyalty.features.loyalty_anti_fraud.name",
|
|
description_key="loyalty.features.loyalty_anti_fraud.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="lock",
|
|
display_order=70,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_google_wallet",
|
|
name_key="loyalty.features.loyalty_google_wallet.name",
|
|
description_key="loyalty.features.loyalty_google_wallet.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="smartphone",
|
|
display_order=80,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_apple_wallet",
|
|
name_key="loyalty.features.loyalty_apple_wallet.name",
|
|
description_key="loyalty.features.loyalty_apple_wallet.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="smartphone",
|
|
display_order=85,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_stats",
|
|
name_key="loyalty.features.loyalty_stats.name",
|
|
description_key="loyalty.features.loyalty_stats.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="bar-chart",
|
|
display_order=90,
|
|
),
|
|
FeatureDeclaration(
|
|
code="loyalty_reports",
|
|
name_key="loyalty.features.loyalty_reports.name",
|
|
description_key="loyalty.features.loyalty_reports.description",
|
|
category="loyalty",
|
|
feature_type=FeatureType.BINARY,
|
|
scope=FeatureScope.MERCHANT,
|
|
ui_icon="file-text",
|
|
display_order=100,
|
|
),
|
|
]
|
|
|
|
def get_store_usage(
|
|
self,
|
|
db: Session,
|
|
store_id: int,
|
|
) -> list[FeatureUsage]:
|
|
if db is None:
|
|
return []
|
|
|
|
from sqlalchemy import func
|
|
|
|
from app.modules.loyalty.models import LoyaltyCard, StaffPin
|
|
|
|
active_cards = (
|
|
db.query(func.count(LoyaltyCard.id))
|
|
.filter(
|
|
LoyaltyCard.enrolled_at_store_id == store_id,
|
|
LoyaltyCard.is_active == True,
|
|
)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
active_pins = (
|
|
db.query(func.count(StaffPin.id))
|
|
.filter(
|
|
StaffPin.store_id == store_id,
|
|
StaffPin.is_active == True,
|
|
)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
return [
|
|
FeatureUsage(feature_code="loyalty_cards", current_usage=active_cards),
|
|
FeatureUsage(feature_code="loyalty_staff_pins", current_usage=active_pins),
|
|
]
|
|
|
|
def get_merchant_usage(
|
|
self,
|
|
db: Session,
|
|
merchant_id: int,
|
|
platform_id: int,
|
|
) -> list[FeatureUsage]:
|
|
if db is None:
|
|
return []
|
|
|
|
from sqlalchemy import func
|
|
|
|
from app.modules.loyalty.models import (
|
|
AppleDeviceRegistration,
|
|
LoyaltyCard,
|
|
StaffPin,
|
|
)
|
|
|
|
active_cards = (
|
|
db.query(func.count(LoyaltyCard.id))
|
|
.filter(
|
|
LoyaltyCard.merchant_id == merchant_id,
|
|
LoyaltyCard.is_active == True,
|
|
)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
active_pins = (
|
|
db.query(func.count(StaffPin.id))
|
|
.filter(
|
|
StaffPin.merchant_id == merchant_id,
|
|
StaffPin.is_active == True,
|
|
)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
apple_registrations = (
|
|
db.query(func.count(AppleDeviceRegistration.id))
|
|
.join(LoyaltyCard)
|
|
.filter(LoyaltyCard.merchant_id == merchant_id)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
google_wallets = (
|
|
db.query(func.count(LoyaltyCard.id))
|
|
.filter(
|
|
LoyaltyCard.merchant_id == merchant_id,
|
|
LoyaltyCard.google_object_id.isnot(None),
|
|
)
|
|
.scalar()
|
|
or 0
|
|
)
|
|
|
|
return [
|
|
FeatureUsage(feature_code="loyalty_cards", current_usage=active_cards),
|
|
FeatureUsage(feature_code="loyalty_staff_pins", current_usage=active_pins),
|
|
FeatureUsage(feature_code="loyalty_apple_wallet", current_usage=apple_registrations),
|
|
FeatureUsage(feature_code="loyalty_google_wallet", current_usage=google_wallets),
|
|
]
|
|
|
|
|
|
# Singleton instance for module registration
|
|
loyalty_feature_provider = LoyaltyFeatureProvider()
|
|
|
|
__all__ = [
|
|
"LoyaltyFeatureProvider",
|
|
"loyalty_feature_provider",
|
|
]
|