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

@@ -234,12 +234,11 @@ class AppleWalletService:
"pass.json": json.dumps(pass_data).encode("utf-8"),
}
# Add placeholder images (in production, these would be actual images)
# For now, we'll skip images and use the pass.json only
# pass_files["icon.png"] = self._get_icon_bytes(program)
# pass_files["icon@2x.png"] = self._get_icon_bytes(program, scale=2)
# pass_files["logo.png"] = self._get_logo_bytes(program)
# pass_files["logo@2x.png"] = self._get_logo_bytes(program, scale=2)
# Add pass images (icon and logo)
pass_files["icon.png"] = self._get_icon_bytes(program)
pass_files["icon@2x.png"] = self._get_icon_bytes(program, scale=2)
pass_files["logo.png"] = self._get_logo_bytes(program)
pass_files["logo@2x.png"] = self._get_logo_bytes(program, scale=2)
# Create manifest
manifest = {}
@@ -377,6 +376,105 @@ class AppleWalletService:
return pass_data
def _get_icon_bytes(self, program: LoyaltyProgram, scale: int = 1) -> bytes:
"""
Generate icon image for Apple Wallet pass.
Apple icon dimensions: 29x29 (@1x), 58x58 (@2x).
Uses program logo if available, otherwise generates a colored square
with the program initial.
"""
from PIL import Image, ImageDraw, ImageFont
size = 29 * scale
if program.logo_url:
try:
import httpx
resp = httpx.get(program.logo_url, timeout=10, follow_redirects=True)
resp.raise_for_status()
img = Image.open(io.BytesIO(resp.content))
img = img.convert("RGBA")
img = img.resize((size, size), Image.LANCZOS)
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
except Exception:
logger.warning("Failed to fetch logo for icon, using fallback")
# Fallback: colored square with initial
hex_color = (program.card_color or "#4F46E5").lstrip("#")
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
img = Image.new("RGBA", (size, size), (r, g, b, 255))
draw = ImageDraw.Draw(img)
initial = (program.display_name or "L")[0].upper()
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", size // 2)
except OSError:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), initial, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
draw.text(((size - tw) / 2, (size - th) / 2 - bbox[1]), initial, fill=(255, 255, 255, 255), font=font)
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
def _get_logo_bytes(self, program: LoyaltyProgram, scale: int = 1) -> bytes:
"""
Generate logo image for Apple Wallet pass.
Apple logo dimensions: 160x50 (@1x), 320x100 (@2x).
Uses program logo if available, otherwise generates a colored rectangle
with the program initial.
"""
from PIL import Image, ImageDraw, ImageFont
width, height = 160 * scale, 50 * scale
if program.logo_url:
try:
import httpx
resp = httpx.get(program.logo_url, timeout=10, follow_redirects=True)
resp.raise_for_status()
img = Image.open(io.BytesIO(resp.content))
img = img.convert("RGBA")
# Fit within dimensions preserving aspect ratio
img.thumbnail((width, height), Image.LANCZOS)
# Center on transparent canvas
canvas = Image.new("RGBA", (width, height), (0, 0, 0, 0))
x = (width - img.width) // 2
y = (height - img.height) // 2
canvas.paste(img, (x, y))
buf = io.BytesIO()
canvas.save(buf, format="PNG")
return buf.getvalue()
except Exception:
logger.warning("Failed to fetch logo for pass logo, using fallback")
# Fallback: colored rectangle with initial
hex_color = (program.card_color or "#4F46E5").lstrip("#")
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
img = Image.new("RGBA", (width, height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
initial = (program.display_name or "L")[0].upper()
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", height // 2)
except OSError:
font = ImageFont.load_default()
bbox = draw.textbbox((0, 0), initial, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
# Draw initial centered
draw.text(((width - tw) / 2, (height - th) / 2 - bbox[1]), initial, fill=(r, g, b, 255), font=font)
buf = io.BytesIO()
img.save(buf, format="PNG")
return buf.getvalue()
def _hex_to_rgb(self, hex_color: str) -> str:
"""Convert hex color to RGB format for Apple Wallet."""
hex_color = hex_color.lstrip("#")