fix: show target-language-specific translations in preview + rename migration
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled

Two issues found in screenshot review:
1. Glossary preview always showed English 'target' field (foie → liver)
   instead of the language-specific translation. Now looks up
   term.translations[targetLang] first, falls back to term.target.
   When target is Persian, preview shows 'foie → کبد' (Persian).

2. Previous migration b7c8d9e0f1a2 was already applied on server before
   the rename was added. New migration c8d9e0f1a2b3 handles the rename
   separately: glossaries with target_language='multi' get their name
   changed from 'Anglais' to 'Multilingue'.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 23:22:50 +02:00
parent a79ce0fc9b
commit b4096fd2ca
4 changed files with 434 additions and 11 deletions

View File

@@ -268,16 +268,74 @@ async def handle_webhook(payload: bytes, sig_header: str) -> Dict[str, Any]:
async def handle_checkout_completed(session: Dict):
"""Handle successful checkout"""
metadata = session.get("metadata", {})
metadata = session.get("metadata", {}) or {}
user_id = metadata.get("user_id")
if not user_id:
return
session_id = session.get("id")
# Check for duplicate session processing using PaymentHistory
db_available = False
try:
from database.connection import get_sync_session
from database.models import PaymentHistory as DBPaymentHistory
db_available = True
except ImportError:
pass
if db_available and session_id:
try:
with get_sync_session() as db_session:
existing = db_session.query(DBPaymentHistory).filter(
DBPaymentHistory.stripe_payment_intent_id == session_id
).first()
if existing:
logger.info("Checkout session %s already processed. Skipping.", session_id)
return
except Exception as e:
logger.error("Error checking PaymentHistory duplication: %s", e)
user = get_user_by_id(user_id)
if not user:
return
# Check if it's a credit purchase
if metadata.get("type") == "credits":
credits = int(metadata.get("credits", 0))
add_credits(user_id, credits)
# Log to PaymentHistory
if db_available and session_id:
try:
with get_sync_session() as db_session:
payment = DBPaymentHistory(
user_id=user_id,
stripe_payment_intent_id=session_id,
stripe_invoice_id=session.get("invoice") or session.get("subscription"),
amount_cents=session.get("amount_total") or 0,
currency=session.get("currency") or "usd",
payment_type="credits",
status="succeeded",
description=f"Achat de {credits} crédits",
)
db_session.add(payment)
db_session.commit()
except Exception as e:
logger.error("Failed to write credits payment to history: %s", e)
# Send Email
try:
from services.email_service import send_subscription_email_async
await send_subscription_email_async(
to_email=user.email,
user_name=user.name,
event_type="credits_purchased",
details={"credits": credits}
)
except Exception as e:
logger.error("Failed to send credit purchase email: %s", e)
return
# It's a subscription
@@ -310,6 +368,12 @@ async def handle_checkout_completed(session: Dict):
# Derive tier from plan (DB constraint: only 'free' or 'pro')
tier = "pro" if plan in ("pro", "business", "enterprise") else "free"
is_new_sub = (
user.stripe_subscription_id != subscription_id
or user.subscription_status != SubscriptionStatus.ACTIVE.value
or user.plan != plan
)
update_user(user_id, {
"plan": plan,
"tier": tier,
@@ -323,14 +387,51 @@ async def handle_checkout_completed(session: Dict):
})
logger.info("Checkout synced: user %s → plan=%s tier=%s sub=%s", user_id, plan, tier, subscription_id)
# Log to PaymentHistory
if db_available and session_id:
try:
with get_sync_session() as db_session:
payment = DBPaymentHistory(
user_id=user_id,
stripe_payment_intent_id=session_id,
stripe_invoice_id=subscription_id or session.get("invoice"),
amount_cents=session.get("amount_total") or 0,
currency=session.get("currency") or "usd",
payment_type="subscription",
status="succeeded",
description=f"Abonnement au forfait {plan}",
)
db_session.add(payment)
db_session.commit()
except Exception as e:
logger.error("Failed to write subscription payment to history: %s", e)
# Send activation email
if is_new_sub:
try:
from services.email_service import send_subscription_email_async
ends_str = subscription_ends_at.strftime("%d/%m/%Y") if subscription_ends_at else ""
await send_subscription_email_async(
to_email=user.email,
user_name=user.name,
event_type="activated",
details={"plan_name": plan, "ends_at": ends_str}
)
except Exception as e:
logger.error("Failed to send activation email: %s", e)
async def handle_subscription_updated(subscription: Dict):
"""Handle subscription updates"""
metadata = subscription.get("metadata", {})
metadata = subscription.get("metadata", {}) or {}
user_id = metadata.get("user_id")
if not user_id:
return
user = get_user_by_id(user_id)
if not user:
return
status_map = {
"active": SubscriptionStatus.ACTIVE,
@@ -343,38 +444,79 @@ async def handle_subscription_updated(subscription: Dict):
stripe_status = subscription.get("status", "active")
status = status_map.get(stripe_status, SubscriptionStatus.ACTIVE)
stripe_cancel_at_period_end = subscription.get("cancel_at_period_end", False)
is_newly_cancelling = stripe_cancel_at_period_end and not user.cancel_at_period_end
period_end = subscription.get("current_period_end")
ends_str = ""
if period_end:
from datetime import timezone
ends_str = datetime.fromtimestamp(period_end, tz=timezone.utc).strftime("%d/%m/%Y")
update_user(user_id, {
"subscription_status": status.value,
"cancel_at_period_end": subscription.get("cancel_at_period_end", False),
"cancel_at_period_end": stripe_cancel_at_period_end,
"subscription_ends_at": datetime.fromtimestamp(
subscription.get("current_period_end", 0)
).isoformat() if subscription.get("current_period_end") else None
})
# Send cancellation email if they just selected to cancel
if is_newly_cancelling:
try:
from services.email_service import send_subscription_email_async
await send_subscription_email_async(
to_email=user.email,
user_name=user.name,
event_type="cancelled",
details={"ends_at": ends_str}
)
except Exception as e:
logger.error("Failed to send cancellation email in handle_subscription_updated: %s", e)
async def handle_subscription_deleted(subscription: Dict):
"""Handle subscription cancellation"""
metadata = subscription.get("metadata", {})
"""Handle subscription cancellation (actually ended)"""
metadata = subscription.get("metadata", {}) or {}
user_id = metadata.get("user_id")
if not user_id:
return
user = get_user_by_id(user_id)
if not user:
return
had_active_sub = user.plan != PlanType.FREE.value or user.tier != "free"
update_user(user_id, {
"plan": PlanType.FREE.value,
"tier": "free",
"subscription_status": SubscriptionStatus.CANCELED.value,
"stripe_subscription_id": None,
})
# Send ended email
if had_active_sub:
try:
from services.email_service import send_subscription_email_async
await send_subscription_email_async(
to_email=user.email,
user_name=user.name,
event_type="ended",
details={}
)
except Exception as e:
logger.error("Failed to send subscription ended email: %s", e)
async def handle_payment_failed(invoice: Dict):
"""Handle failed payment — set subscription status to PAST_DUE"""
"""Handle failed payment — set subscription status to PAST_DUE and send email"""
customer_id = invoice.get("customer")
if not customer_id:
return
# Find user by stripe_customer_id
user = None
try:
from database.connection import get_sync_session
from database.models import User as DBUser
@@ -393,6 +535,18 @@ async def handle_payment_failed(invoice: Dict):
"Payment failed for customer %s (user %s) — status set to past_due",
customer_id, user_id,
)
# Send email
try:
from services.email_service import send_subscription_email_async
await send_subscription_email_async(
to_email=db_user.email,
user_name=db_user.name,
event_type="payment_failed",
details={}
)
except Exception as e:
logger.error("Failed to send payment failed email: %s", e)
except Exception as exc:
logger.error("handle_payment_failed DB error: %s", exc)
@@ -417,14 +571,31 @@ async def cancel_subscription(user_id: str) -> Dict[str, Any]:
cancel_at = datetime.fromtimestamp(subscription.cancel_at).isoformat()
subscription_ends_at = None
ends_str = ""
if subscription.current_period_end:
subscription_ends_at = datetime.fromtimestamp(subscription.current_period_end).isoformat()
ends_str = datetime.fromtimestamp(subscription.current_period_end).strftime("%d/%m/%Y")
is_new_cancel = not user.cancel_at_period_end
update_user(user_id, {
"cancel_at_period_end": True,
"subscription_ends_at": subscription_ends_at,
})
# Send cancellation confirmation email
if is_new_cancel:
try:
from services.email_service import send_subscription_email_async
await send_subscription_email_async(
to_email=user.email,
user_name=user.name,
event_type="cancelled",
details={"ends_at": ends_str}
)
except Exception as e:
logger.error("Failed to send cancellation email in cancel_subscription: %s", e)
return {
"status": "canceling",
"cancel_at": cancel_at,