From 4293b498496ae7011ca91e2a5223bd0e8a3e2e8f Mon Sep 17 00:00:00 2001 From: NguyenND Date: Sun, 28 Jun 2026 17:54:06 -0400 Subject: [PATCH] Jun 28 - Update tenant bill management --- app/billing/webhooks.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/app/billing/webhooks.py b/app/billing/webhooks.py index 7b88450..77053f9 100644 --- a/app/billing/webhooks.py +++ b/app/billing/webhooks.py @@ -109,20 +109,24 @@ def _on_checkout_completed(obj): def _on_subscription_updated(obj): - """customer.subscription.updated — sync status from Stripe.""" + """customer.subscription.updated — sync status and plan from Stripe.""" customer_id = obj.get('customer') subscription_id = obj.get('id') stripe_status = obj.get('status', '') period_end_ts = obj.get('current_period_end') trial_end_ts = obj.get('trial_end') + # Extract the price ID from the first subscription item (plan change detection). + items = obj.get('items', {}).get('data', []) + new_price_id = items[0].get('price', {}).get('id') if items else None + our_status = _STRIPE_STATUS_MAP.get(stripe_status) if our_status is None: logger.warning('BILLING | subscription_updated_unknown_status | status=%s', stripe_status) return from control.base import control_session - from control.models import Tenant, TenantAudit + from control.models import Tenant, Plan, TenantAudit with control_session() as s: t = s.query(Tenant).filter_by(stripe_customer_id=customer_id).first() @@ -130,17 +134,29 @@ def _on_subscription_updated(obj): logger.warning('BILLING | subscription_updated_no_tenant | customer=%s', customer_id) return - old_status = t.subscription_status - t.subscription_status = our_status + old_status = t.subscription_status + old_plan_id = t.plan_id + + t.subscription_status = our_status t.stripe_subscription_id = subscription_id - t.current_period_end = _ts_to_dt(period_end_ts) + t.current_period_end = _ts_to_dt(period_end_ts) if trial_end_ts: t.trial_ends_at = _ts_to_dt(trial_end_ts) + # Sync plan when the price changes (upgrade / downgrade via portal). + plan_detail = '' + if new_price_id: + matched_plan = s.query(Plan).filter_by(stripe_price_id=new_price_id).first() + if matched_plan and matched_plan.id != old_plan_id: + t.plan_id = matched_plan.id + plan_detail = f' plan={old_plan_id}→{matched_plan.id}({matched_plan.code})' + logger.info('BILLING | plan_changed | tenant_id=%s plan=%s→%s', + t.id, old_plan_id, matched_plan.id) + s.add(TenantAudit( action='billing_subscription_updated', tenant_id=t.id, - details=f'stripe_status={stripe_status} our_status={our_status} prev={old_status}', + details=f'stripe_status={stripe_status} our_status={our_status} prev={old_status}{plan_detail}', )) logger.info('BILLING | subscription_updated | tenant_id=%s %s→%s', t.id, old_status, our_status)