feat: add SQL query tool, platform debug, loyalty settings, and multi-module improvements
Some checks failed
CI / ruff (push) Successful in 14s
CI / pytest (push) Failing after 50m12s
CI / validate (push) Successful in 25s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- Add admin SQL query tool with saved queries, schema explorer presets,
  and collapsible category sections (dev_tools module)
- Add platform debug tool for admin diagnostics
- Add loyalty settings page with owner-only access control
- Fix loyalty settings owner check (use currentUser instead of window.__userData)
- Replace HTTPException with AuthorizationException in loyalty routes
- Expand loyalty module with PIN service, Apple Wallet, program management
- Improve store login with platform detection and multi-platform support
- Update billing feature gates and subscription services
- Add store platform sync improvements and remove is_primary column
- Add unit tests for loyalty (PIN, points, stamps, program services)
- Update i18n translations across dev_tools locales

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 20:08:07 +01:00
parent a77a8a3a98
commit 319900623a
77 changed files with 5341 additions and 401 deletions

View File

@@ -352,3 +352,98 @@ class TestDeleteProgram:
"""Deleting non-existent program raises exception."""
with pytest.raises(LoyaltyProgramNotFoundException):
self.service.delete_program(db, 999999)
# ============================================================================
# Stats
# ============================================================================
@pytest.mark.unit
@pytest.mark.loyalty
class TestGetProgramStats:
"""Tests for get_program_stats."""
def setup_method(self):
self.service = ProgramService()
def test_stats_returns_all_fields(self, db, ps_program):
"""Stats response includes all required fields."""
stats = self.service.get_program_stats(db, ps_program.id)
assert "total_cards" in stats
assert "active_cards" in stats
assert "new_this_month" in stats
assert "total_points_balance" in stats
assert "avg_points_per_member" in stats
assert "transactions_30d" in stats
assert "points_issued_30d" in stats
assert "points_redeemed_30d" in stats
assert "points_this_month" in stats
assert "points_redeemed_this_month" in stats
assert "estimated_liability_cents" in stats
def test_stats_empty_program(self, db, ps_program):
"""Stats for program with no cards."""
stats = self.service.get_program_stats(db, ps_program.id)
assert stats["total_cards"] == 0
assert stats["active_cards"] == 0
assert stats["new_this_month"] == 0
assert stats["total_points_balance"] == 0
assert stats["avg_points_per_member"] == 0
def test_stats_with_cards(self, db, ps_program, ps_merchant):
"""Stats reflect actual card data."""
from datetime import UTC, datetime
from app.modules.customers.models.customer import Customer
from app.modules.loyalty.models import LoyaltyCard
from app.modules.tenancy.models import Store
uid_store = uuid.uuid4().hex[:8]
store = Store(
merchant_id=ps_merchant.id,
store_code=f"STAT_{uid_store.upper()}",
subdomain=f"stat{uid_store}",
name=f"Stats Store {uid_store}",
is_active=True,
is_verified=True,
)
db.add(store)
db.flush()
# Create cards with customers
for i in range(3):
uid = uuid.uuid4().hex[:8]
customer = Customer(
email=f"stat_{uid}@test.com",
first_name="Stat",
last_name=f"Customer{i}",
hashed_password="!unused!", # noqa: SEC001
customer_number=f"SC-{uid.upper()}",
store_id=store.id,
is_active=True,
)
db.add(customer)
db.flush()
card = LoyaltyCard(
merchant_id=ps_merchant.id,
program_id=ps_program.id,
customer_id=customer.id,
card_number=f"STAT-{i}-{uuid.uuid4().hex[:6]}",
points_balance=100 * (i + 1),
total_points_earned=100 * (i + 1),
is_active=True,
last_activity_at=datetime.now(UTC),
)
db.add(card)
db.commit()
stats = self.service.get_program_stats(db, ps_program.id)
assert stats["total_cards"] == 3
assert stats["active_cards"] == 3
assert stats["total_points_balance"] == 600 # 100+200+300
assert stats["avg_points_per_member"] == 200.0 # 600/3