rairo commited on
Commit
6ce11bc
·
verified ·
1 Parent(s): 601f04b

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +209 -54
main.py CHANGED
@@ -256,14 +256,31 @@ logging.basicConfig(level=logging.INFO)
256
  logger = logging.getLogger(__name__)
257
 
258
  ## Stripe
 
259
  PLAN_CONFIG = {
260
  # The Fixer – standard
261
- "standard": {"price_id": STRIPE_PRICE_FIXER, "credits": 200},
262
- "fixer": {"price_id": STRIPE_PRICE_FIXER, "credits": 200},
 
 
 
 
 
 
 
 
263
 
264
  # The Pro – premium
265
- "premium": {"price_id": STRIPE_PRICE_PRO, "credits": 500},
266
- "pro": {"price_id": STRIPE_PRICE_PRO, "credits": 500},
 
 
 
 
 
 
 
 
267
  }
268
 
269
  def get_or_create_stripe_customer(uid: str) -> str:
@@ -287,29 +304,86 @@ def get_or_create_stripe_customer(uid: str) -> str:
287
  user_ref.update({"stripeCustomerId": customer.id})
288
  return customer.id
289
 
290
-
291
- def apply_plan_credits(uid: str, plan_key: str):
292
  """
293
- Adds the correct number of credits for a plan
294
- (200 for standard, 500 for premium).
 
 
 
 
295
  """
296
  plan_cfg = PLAN_CONFIG.get(plan_key)
297
  if not plan_cfg:
298
  logger.error(f"[STRIPE] Unknown plan '{plan_key}' for user {uid}")
299
  return
300
 
301
- credits_to_add = plan_cfg["credits"]
 
302
  user_ref = db_ref.child(f'users/{uid}')
303
  user_data = user_ref.get() or {}
304
- current = user_data.get("credits", 0)
 
 
 
 
 
 
305
 
306
- new_total = current + credits_to_add
307
- user_ref.update({"credits": new_total})
308
  logger.info(
309
- f"[STRIPE] Applied {credits_to_add} credits to user {uid} "
310
- f"for plan '{plan_key}'. New total: {new_total}"
311
  )
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  # =============================================================================
314
  # OPEN IMAGE PROXY ENDPOINT (NO AUTHENTICATION)
315
  # =============================================================================
@@ -1375,14 +1449,14 @@ def log_call_usage(project_id):
1375
  def get_stripe_config():
1376
  """
1377
  Returns safe-to-expose Stripe configuration values for the frontend.
1378
- (No secret keys only publishable data)
1379
  """
1380
  try:
1381
  return jsonify({
1382
  "publishableKey": os.environ.get("STRIPE_PUBLISHABLE_KEY"),
1383
  "priceIds": {
1384
- "fixer": os.environ.get("STRIPE_PRICE_FIXER"),
1385
- "pro": os.environ.get("STRIPE_PRICE_PRO"),
1386
  }
1387
  }), 200
1388
  except Exception as e:
@@ -1394,9 +1468,10 @@ def get_stripe_config():
1394
  def create_checkout_session():
1395
  """
1396
  Creates a Stripe Checkout Session for a recurring subscription.
1397
- Plans:
1398
- - 'standard' / 'fixer' → The Fixer (200 credits / month)
1399
- - 'premium' / 'pro' → The Pro (500 credits / month)
 
1400
  """
1401
  uid = verify_token(request.headers.get("Authorization"))
1402
  if not uid:
@@ -1407,12 +1482,17 @@ def create_checkout_session():
1407
  return jsonify({"error": "Stripe is not configured on the server."}), 500
1408
 
1409
  data = request.get_json() or {}
1410
- plan = (data.get("plan") or "").lower().strip()
1411
 
1412
- plan_cfg = PLAN_CONFIG.get(plan)
1413
- if not plan_cfg or not plan_cfg["price_id"]:
1414
  return jsonify({"error": "Invalid plan selected."}), 400
1415
 
 
 
 
 
 
1416
  try:
1417
  customer_id = get_or_create_stripe_customer(uid)
1418
 
@@ -1421,29 +1501,30 @@ def create_checkout_session():
1421
  customer=customer_id,
1422
  payment_method_types=["card"],
1423
  line_items=[{
1424
- "price": plan_cfg["price_id"],
1425
  "quantity": 1,
1426
  }],
1427
  success_url=STRIPE_SUCCESS_URL + "?session_id={CHECKOUT_SESSION_ID}",
1428
  cancel_url=STRIPE_CANCEL_URL,
1429
- # So we can find the user/plan again from webhooks
1430
  metadata={
1431
  "firebase_uid": uid,
1432
- "plan": plan,
1433
  },
1434
  subscription_data={
1435
  "metadata": {
1436
  "firebase_uid": uid,
1437
- "plan": plan,
1438
  }
1439
  },
1440
  )
1441
 
1442
- logger.info(f"[STRIPE] Created checkout session {session.id} for user {uid}, plan {plan}")
 
 
1443
  return jsonify({
1444
  "id": session.id,
1445
  "url": session.url,
1446
- })
1447
 
1448
  except Exception as e:
1449
  logger.error(f"[STRIPE] Error creating checkout session: {e}")
@@ -1451,6 +1532,18 @@ def create_checkout_session():
1451
 
1452
  @app.route("/api/billing/webhook", methods=["POST"])
1453
  def stripe_webhook():
 
 
 
 
 
 
 
 
 
 
 
 
1454
  payload = request.data
1455
  sig_header = request.headers.get("Stripe-Signature")
1456
 
@@ -1458,36 +1551,98 @@ def stripe_webhook():
1458
  event = stripe.Webhook.construct_event(
1459
  payload, sig_header, STRIPE_WEBHOOK_SECRET
1460
  )
 
 
 
1461
  except Exception as e:
1462
- logger.error(f"[STRIPE] Webhook error: {e}")
1463
- return "Bad", 400
1464
 
1465
  event_type = event.get("type")
1466
- obj = event.get("data", {}).get("object", {})
 
1467
  logger.info(f"[STRIPE] Webhook event: {event_type}")
1468
 
1469
- # Handle subscription billing
1470
- if event_type == "invoice.payment_succeeded":
1471
- invoice = obj
1472
- subscription_id = invoice.get("subscription")
1473
- if subscription_id:
1474
- sub = stripe.Subscription.retrieve(subscription_id)
1475
- md = sub.get("metadata", {}) or {}
1476
- uid = md.get("firebase_uid")
1477
- plan = md.get("plan")
1478
- logger.info(f"[STRIPE] invoice.payment_succeeded uid={uid}, plan={plan}")
1479
- if uid and plan:
1480
- apply_plan_credits(uid, plan)
1481
-
1482
- # Also handle checkout.session.completed as a fallback
1483
- if event_type == "checkout.session.completed":
1484
- session = obj
1485
- md = session.get("metadata", {}) or {}
1486
- uid = md.get("firebase_uid")
1487
- plan = md.get("plan")
1488
- logger.info(f"[STRIPE] checkout.session.completed uid={uid}, plan={plan}")
1489
- if uid and plan:
1490
- apply_plan_credits(uid, plan)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1491
 
1492
  return "", 200
1493
  # -----------------------------------------------------------------------------
 
256
  logger = logging.getLogger(__name__)
257
 
258
  ## Stripe
259
+ # Stripe plan config
260
  PLAN_CONFIG = {
261
  # The Fixer – standard
262
+ "standard": {
263
+ "price_id": STRIPE_PRICE_FIXER,
264
+ "credits": 200,
265
+ "label": "The Fixer",
266
+ },
267
+ "fixer": {
268
+ "price_id": STRIPE_PRICE_FIXER,
269
+ "credits": 200,
270
+ "label": "The Fixer",
271
+ },
272
 
273
  # The Pro – premium
274
+ "premium": {
275
+ "price_id": STRIPE_PRICE_PRO,
276
+ "credits": 500,
277
+ "label": "The Pro",
278
+ },
279
+ "pro": {
280
+ "price_id": STRIPE_PRICE_PRO,
281
+ "credits": 500,
282
+ "label": "The Pro",
283
+ },
284
  }
285
 
286
  def get_or_create_stripe_customer(uid: str) -> str:
 
304
  user_ref.update({"stripeCustomerId": customer.id})
305
  return customer.id
306
 
307
+ def reset_plan_credits(uid: str, plan_key: str):
 
308
  """
309
+ HARD RESET the user's credits to the monthly allowance for the given plan.
310
+ NO ROLLOVER:
311
+ - standard / fixer -> 200
312
+ - premium / pro -> 500
313
+
314
+ Any previous balance is overwritten.
315
  """
316
  plan_cfg = PLAN_CONFIG.get(plan_key)
317
  if not plan_cfg:
318
  logger.error(f"[STRIPE] Unknown plan '{plan_key}' for user {uid}")
319
  return
320
 
321
+ monthly_credits = float(plan_cfg["credits"])
322
+
323
  user_ref = db_ref.child(f'users/{uid}')
324
  user_data = user_ref.get() or {}
325
+ previous = user_data.get('credits', 0)
326
+
327
+ user_ref.update({
328
+ "credits": monthly_credits,
329
+ "lastCreditResetAt": datetime.utcnow().isoformat(),
330
+ "creditResetPlan": plan_key,
331
+ })
332
 
 
 
333
  logger.info(
334
+ f"[STRIPE] RESET credits for user {uid} to {monthly_credits} "
335
+ f"for plan '{plan_key}' (no rollover). Previous balance: {previous}"
336
  )
337
 
338
+ def update_user_subscription_from_stripe(subscription: dict):
339
+ """
340
+ Syncs Stripe subscription info into the Firebase user document.
341
+
342
+ Stores:
343
+ - currentPlan (key: standard/premium)
344
+ - currentPlanLabel ("The Fixer"/"The Pro")
345
+ - currentPlanMonthlyCredits (200/500)
346
+ - planStatus (active/past_due/canceled/unpaid/etc.)
347
+ - planCurrentPeriodEnd (ISO expiry for current period)
348
+ - stripeSubscriptionId
349
+ - planUpdatedAt
350
+ """
351
+ metadata = subscription.get("metadata") or {}
352
+ uid = metadata.get("firebase_uid")
353
+ plan_key = metadata.get("plan")
354
+
355
+ if not uid or not plan_key:
356
+ logger.error("[STRIPE] Subscription missing firebase_uid or plan in metadata.")
357
+ return
358
+
359
+ plan_cfg = PLAN_CONFIG.get(plan_key, {})
360
+ plan_label = plan_cfg.get("label", plan_key.title())
361
+ monthly_credits = plan_cfg.get("credits", 0)
362
+
363
+ status = subscription.get("status") # active, past_due, canceled, unpaid, trialing, etc.
364
+ current_period_end = subscription.get("current_period_end") # Unix timestamp
365
+
366
+ if current_period_end:
367
+ expiry_iso = datetime.utcfromtimestamp(current_period_end).isoformat() + "Z"
368
+ else:
369
+ expiry_iso = None
370
+
371
+ user_ref = db_ref.child(f'users/{uid}')
372
+ update_data = {
373
+ "currentPlan": plan_key, # "standard" / "premium"
374
+ "currentPlanLabel": plan_label, # "The Fixer" / "The Pro"
375
+ "currentPlanMonthlyCredits": monthly_credits, # 200 / 500
376
+ "planStatus": status, # active / past_due / canceled / unpaid ...
377
+ "planCurrentPeriodEnd": expiry_iso, # ISO timestamp of current period end
378
+ "stripeSubscriptionId": subscription.get("id"),
379
+ "planUpdatedAt": datetime.utcnow().isoformat(),
380
+ }
381
+
382
+ logger.info(
383
+ f"[STRIPE] Updating subscription for user {uid}: "
384
+ f"plan={plan_key}, status={status}, expires={expiry_iso}"
385
+ )
386
+ user_ref.update(update_data)
387
  # =============================================================================
388
  # OPEN IMAGE PROXY ENDPOINT (NO AUTHENTICATION)
389
  # =============================================================================
 
1449
  def get_stripe_config():
1450
  """
1451
  Returns safe-to-expose Stripe configuration values for the frontend.
1452
+ No secret keys are exposed here.
1453
  """
1454
  try:
1455
  return jsonify({
1456
  "publishableKey": os.environ.get("STRIPE_PUBLISHABLE_KEY"),
1457
  "priceIds": {
1458
+ "fixer": STRIPE_PRICE_FIXER,
1459
+ "pro": STRIPE_PRICE_PRO,
1460
  }
1461
  }), 200
1462
  except Exception as e:
 
1468
  def create_checkout_session():
1469
  """
1470
  Creates a Stripe Checkout Session for a recurring subscription.
1471
+
1472
+ Client body:
1473
+ { "plan": "standard" } -> The Fixer (200 credits/month)
1474
+ { "plan": "premium" } -> The Pro (500 credits/month)
1475
  """
1476
  uid = verify_token(request.headers.get("Authorization"))
1477
  if not uid:
 
1482
  return jsonify({"error": "Stripe is not configured on the server."}), 500
1483
 
1484
  data = request.get_json() or {}
1485
+ plan_key = (data.get("plan") or "").lower().strip()
1486
 
1487
+ plan_cfg = PLAN_CONFIG.get(plan_key)
1488
+ if not plan_cfg:
1489
  return jsonify({"error": "Invalid plan selected."}), 400
1490
 
1491
+ price_id = plan_cfg.get("price_id")
1492
+ if not price_id or not price_id.startswith("price_"):
1493
+ logger.error(f"[STRIPE] Misconfigured price_id for plan {plan_key}: {price_id}")
1494
+ return jsonify({"error": "Billing configuration error – contact support."}), 500
1495
+
1496
  try:
1497
  customer_id = get_or_create_stripe_customer(uid)
1498
 
 
1501
  customer=customer_id,
1502
  payment_method_types=["card"],
1503
  line_items=[{
1504
+ "price": price_id,
1505
  "quantity": 1,
1506
  }],
1507
  success_url=STRIPE_SUCCESS_URL + "?session_id={CHECKOUT_SESSION_ID}",
1508
  cancel_url=STRIPE_CANCEL_URL,
 
1509
  metadata={
1510
  "firebase_uid": uid,
1511
+ "plan": plan_key,
1512
  },
1513
  subscription_data={
1514
  "metadata": {
1515
  "firebase_uid": uid,
1516
+ "plan": plan_key,
1517
  }
1518
  },
1519
  )
1520
 
1521
+ logger.info(
1522
+ f"[STRIPE] Created checkout session {session.id} for user {uid}, plan {plan_key}"
1523
+ )
1524
  return jsonify({
1525
  "id": session.id,
1526
  "url": session.url,
1527
+ }), 200
1528
 
1529
  except Exception as e:
1530
  logger.error(f"[STRIPE] Error creating checkout session: {e}")
 
1532
 
1533
  @app.route("/api/billing/webhook", methods=["POST"])
1534
  def stripe_webhook():
1535
+ """
1536
+ Handles Stripe webhook events:
1537
+ - checkout.session.completed -> initial subscription + first credits
1538
+ - invoice.payment_succeeded -> monthly renewals (reset credits)
1539
+ - invoice.payment_failed -> mark planStatus as past_due/unpaid
1540
+ - customer.subscription.deleted -> cancelled subscription
1541
+ - customer.subscription.updated -> keep status/expiry in sync
1542
+ """
1543
+ if not STRIPE_WEBHOOK_SECRET:
1544
+ logger.error("[STRIPE] STRIPE_WEBHOOK_SECRET not set.")
1545
+ return jsonify({"error": "Webhook secret not configured."}), 500
1546
+
1547
  payload = request.data
1548
  sig_header = request.headers.get("Stripe-Signature")
1549
 
 
1551
  event = stripe.Webhook.construct_event(
1552
  payload, sig_header, STRIPE_WEBHOOK_SECRET
1553
  )
1554
+ except stripe.error.SignatureVerificationError as e:
1555
+ logger.error(f"[STRIPE] Webhook signature verification failed: {e}")
1556
+ return "Invalid signature", 400
1557
  except Exception as e:
1558
+ logger.error(f"[STRIPE] Webhook parsing error: {e}")
1559
+ return "Bad request", 400
1560
 
1561
  event_type = event.get("type")
1562
+ data_obj = event.get("data", {}).get("object", {})
1563
+
1564
  logger.info(f"[STRIPE] Webhook event: {event_type}")
1565
 
1566
+ try:
1567
+ # 1) First payment via Checkout
1568
+ if event_type == "checkout.session.completed":
1569
+ session = data_obj
1570
+ metadata = session.get("metadata") or {}
1571
+ uid = metadata.get("firebase_uid")
1572
+ plan_key = metadata.get("plan")
1573
+ subscription_id = session.get("subscription")
1574
+
1575
+ logger.info(
1576
+ f"[STRIPE] checkout.session.completed uid={uid}, plan={plan_key}, sub={subscription_id}"
1577
+ )
1578
+
1579
+ if subscription_id:
1580
+ sub = stripe.Subscription.retrieve(subscription_id)
1581
+ update_user_subscription_from_stripe(sub)
1582
+
1583
+ # First period: reset credits to plan allowance (no rollover)
1584
+ if uid and plan_key:
1585
+ reset_plan_credits(uid, plan_key)
1586
+
1587
+ # 2) Every successful invoice (monthly renewal)
1588
+ elif event_type == "invoice.payment_succeeded":
1589
+ invoice = data_obj
1590
+ subscription_id = invoice.get("subscription")
1591
+
1592
+ if subscription_id:
1593
+ sub = stripe.Subscription.retrieve(subscription_id)
1594
+ metadata = sub.get("metadata") or {}
1595
+ uid = metadata.get("firebase_uid")
1596
+ plan_key = metadata.get("plan")
1597
+
1598
+ logger.info(
1599
+ f"[STRIPE] invoice.payment_succeeded uid={uid}, plan={plan_key}, sub={subscription_id}"
1600
+ )
1601
+
1602
+ update_user_subscription_from_stripe(sub)
1603
+
1604
+ # New billing period: hard reset credits to monthly quota
1605
+ if uid and plan_key:
1606
+ reset_plan_credits(uid, plan_key)
1607
+
1608
+ # 3) Invoice failed (non-payment) – mark plan as not good
1609
+ elif event_type == "invoice.payment_failed":
1610
+ invoice = data_obj
1611
+ subscription_id = invoice.get("subscription")
1612
+
1613
+ if subscription_id:
1614
+ sub = stripe.Subscription.retrieve(subscription_id)
1615
+ logger.info(
1616
+ f"[STRIPE] invoice.payment_failed subscription={subscription_id}, status={sub.get('status')}"
1617
+ )
1618
+ # Status will now be past_due/unpaid; keep in sync
1619
+ update_user_subscription_from_stripe(sub)
1620
+
1621
+ # Optional: if you want to be harsh, you could also zero credits here.
1622
+ # metadata = sub.get("metadata") or {}
1623
+ # uid = metadata.get("firebase_uid")
1624
+ # if uid:
1625
+ # db_ref.child(f'users/{uid}').update({"credits": 0.0})
1626
+ # logger.info(f"[STRIPE] Payment failed – zeroed credits for user {uid}")
1627
+
1628
+ # 4) Subscription deleted (cancelled)
1629
+ elif event_type == "customer.subscription.deleted":
1630
+ sub = data_obj
1631
+ logger.info(
1632
+ f"[STRIPE] customer.subscription.deleted id={sub.get('id')}, status={sub.get('status')}"
1633
+ )
1634
+ update_user_subscription_from_stripe(sub)
1635
+
1636
+ # 5) Subscription updated (status, period, etc.)
1637
+ elif event_type == "customer.subscription.updated":
1638
+ sub = data_obj
1639
+ logger.info(
1640
+ f"[STRIPE] customer.subscription.updated id={sub.get('id')}, status={sub.get('status')}"
1641
+ )
1642
+ update_user_subscription_from_stripe(sub)
1643
+
1644
+ except Exception as e:
1645
+ logger.error(f"[STRIPE] Error handling webhook {event_type}: {e}")
1646
 
1647
  return "", 200
1648
  # -----------------------------------------------------------------------------