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

@@ -115,8 +115,9 @@ class MerchantDomainService:
db.query(StorePlatform)
.filter(
StorePlatform.store_id.in_(store_ids),
StorePlatform.is_primary.is_(True),
StorePlatform.is_active == True, # noqa: E712
)
.order_by(StorePlatform.joined_at)
.first()
)
platform_id = primary_sp.platform_id if primary_sp else None

View File

@@ -324,9 +324,12 @@ class PlatformService:
# ========================================================================
@staticmethod
def get_primary_platform_id_for_store(db: Session, store_id: int) -> int | None:
def get_first_active_platform_id_for_store(db: Session, store_id: int) -> int | None:
"""
Get the primary platform ID for a store.
Get the first active platform ID for a store (ordered by joined_at).
Used as a fallback when platform_id is not available from JWT context
(e.g. background tasks, old tokens).
Args:
db: Database session
@@ -341,7 +344,7 @@ class PlatformService:
StorePlatform.store_id == store_id,
StorePlatform.is_active == True, # noqa: E712
)
.order_by(StorePlatform.is_primary.desc())
.order_by(StorePlatform.joined_at)
.first()
)
return result[0] if result else None
@@ -364,7 +367,7 @@ class PlatformService:
StorePlatform.store_id == store_id,
StorePlatform.is_active == True, # noqa: E712
)
.order_by(StorePlatform.is_primary.desc())
.order_by(StorePlatform.joined_at)
.all()
)
return [r[0] for r in results]
@@ -393,29 +396,6 @@ class PlatformService:
.first()
)
@staticmethod
def get_primary_store_platform_entry(
db: Session, store_id: int
) -> StorePlatform | None:
"""
Get the primary StorePlatform entry for a store.
Args:
db: Database session
store_id: Store ID
Returns:
StorePlatform object or None
"""
return (
db.query(StorePlatform)
.filter(
StorePlatform.store_id == store_id,
StorePlatform.is_primary.is_(True),
)
.first()
)
@staticmethod
def get_store_ids_for_platform(
db: Session, platform_id: int, active_only: bool = True
@@ -450,7 +430,7 @@ class PlatformService:
Upsert a StorePlatform entry.
If the entry exists, update is_active (and tier_id if provided).
If missing and is_active=True, create it (set is_primary if store has none).
If missing and is_active=True, create it.
If missing and is_active=False, no-op.
Args:
@@ -479,20 +459,10 @@ class PlatformService:
return existing
if is_active:
has_primary = (
db.query(StorePlatform)
.filter(
StorePlatform.store_id == store_id,
StorePlatform.is_primary.is_(True),
)
.first()
) is not None
sp = StorePlatform(
store_id=store_id,
platform_id=platform_id,
is_active=True,
is_primary=not has_primary,
tier_id=tier_id,
)
db.add(sp)

View File

@@ -105,16 +105,13 @@ class StoreDomainService:
if domain_data.is_primary:
self._unset_primary_domains(db, store_id)
# Resolve platform_id: use provided value, or auto-resolve from primary StorePlatform
# Resolve platform_id: use provided value, or auto-resolve from first active StorePlatform
platform_id = domain_data.platform_id
if not platform_id:
from app.modules.tenancy.models import StorePlatform
primary_sp = (
db.query(StorePlatform)
.filter(StorePlatform.store_id == store_id, StorePlatform.is_primary.is_(True))
.first()
from app.modules.tenancy.services.platform_service import (
platform_service,
)
platform_id = primary_sp.platform_id if primary_sp else None
platform_id = platform_service.get_first_active_platform_id_for_store(db, store_id)
# Create domain record
new_domain = StoreDomain(