A5-R3-R5C dormant commercial lifecycle

#114
Files changed (3) hide show
  1. app.py +6 -2
  2. i18n.py +12 -0
  3. managed_auth.py +86 -7
app.py CHANGED
@@ -663,11 +663,15 @@ def _process_pending_launch(lang_code: str) -> None:
663
  except Exception as exc:
664
  st.session_state["sp_pending_launch"] = None
665
  raw = str(exc or "")
666
- if "payment_required" in raw:
 
 
 
 
667
  st.session_state["sp_payment_required"] = True
668
  st.error(tr(lang_code, "quota_exhausted"))
669
  render_payment_purchase(lang_code)
670
- elif "a5_product_task_path_not_authorized" in raw:
671
  st.error(tr(lang_code, "product_generation_not_active"))
672
  else:
673
  st.error(raw)
 
663
  except Exception as exc:
664
  st.session_state["sp_pending_launch"] = None
665
  raw = str(exc or "")
666
+ if "product_entitlement_required" in raw:
667
+ st.session_state["sp_payment_required"] = True
668
+ st.error(tr(lang_code, "product_entitlement_required"))
669
+ render_payment_purchase(lang_code)
670
+ elif "payment_required" in raw:
671
  st.session_state["sp_payment_required"] = True
672
  st.error(tr(lang_code, "quota_exhausted"))
673
  render_payment_purchase(lang_code)
674
+ elif "a5_product_task_path_not_active" in raw or "a5_product_task_path_not_authorized" in raw:
675
  st.error(tr(lang_code, "product_generation_not_active"))
676
  else:
677
  st.error(raw)
i18n.py CHANGED
@@ -395,6 +395,9 @@ TEXTS: dict[str, dict[str, str]] = {
395
  "logout_short": "退出",
396
  "payment_dialog_title": "支付宝付款",
397
  "payment_notice": "今日免费额度和长期额度均已用完。支付9.9元可增加3篇长期有效额度。",
 
 
 
398
  "quota_exhausted": "今日免费额度和长期额度均已用完。请使用支付宝购买长期额度。",
399
  "payment_create": "生成支付宝付款二维码",
400
  "payment_creating": "正在创建支付宝订单……",
@@ -602,6 +605,9 @@ TEXTS: dict[str, dict[str, str]] = {
602
  "logout_short": "Sign out",
603
  "payment_dialog_title": "Alipay Payment",
604
  "payment_notice": "Your daily free quota and long-term quota are exhausted. Pay CNY 9.9 to add three long-term papers.",
 
 
 
605
  "quota_exhausted": "Your daily free quota and long-term quota are exhausted. Purchase long-term quota with Alipay.",
606
  "payment_create": "Create Alipay QR code",
607
  "payment_creating": "Creating the Alipay order…",
@@ -809,6 +815,9 @@ TEXTS: dict[str, dict[str, str]] = {
809
  "logout_short": "Выйти",
810
  "payment_dialog_title": "Оплата через Alipay",
811
  "payment_notice": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Оплата 9,9 юаня добавляет квоту на три статьи без срока действия.",
 
 
 
812
  "quota_exhausted": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Приобретите долгосрочную квоту через Alipay.",
813
  "payment_create": "Создать QR-код для оплаты",
814
  "payment_creating": "Создаётся заказ Alipay…",
@@ -1016,6 +1025,9 @@ TEXTS: dict[str, dict[str, str]] = {
1016
  "logout_short": "Шығу",
1017
  "payment_dialog_title": "Alipay арқылы төлеу",
1018
  "payment_notice": "Күнделікті тегін және ұзақ мерзімді квота таусылды. 9,9 юань төлегенде мерзімі шектелмеген үш мақала квотасы қосылады.",
 
 
 
1019
  "quota_exhausted": "Күнделікті тегін және ұзақ мерзімді квота таусылды. Alipay арқылы ұзақ мерзімді квота сатып алыңыз.",
1020
  "payment_create": "Төлемге арналған QR-код жасау",
1021
  "payment_creating": "Alipay тапсырысы жасалуда…",
 
395
  "logout_short": "退出",
396
  "payment_dialog_title": "支付宝付款",
397
  "payment_notice": "今日免费额度和长期额度均已用完。支付9.9元可增加3篇长期有效额度。",
398
+ "product_entitlement_required": "当前所选版本额度不足,请使用支付宝购买对应版本额度。",
399
+ "payment_notice_product": "购买{tier}:支付 {amount} {currency} 可增加{credits}篇长期有效额度。",
400
+ "payment_paid_product": "支付成功,已增加{credits}篇{tier}长期有效额度。请重新点击生成论文。",
401
  "quota_exhausted": "今日免费额度和长期额度均已用完。请使用支付宝购买长期额度。",
402
  "payment_create": "生成支付宝付款二维码",
403
  "payment_creating": "正在创建支付宝订单……",
 
605
  "logout_short": "Sign out",
606
  "payment_dialog_title": "Alipay Payment",
607
  "payment_notice": "Your daily free quota and long-term quota are exhausted. Pay CNY 9.9 to add three long-term papers.",
608
+ "product_entitlement_required": "The selected plan has no remaining credits. Purchase credits for this plan with Alipay.",
609
+ "payment_notice_product": "Purchase {tier}: pay {currency} {amount} to add {credits} long-term paper credits.",
610
+ "payment_paid_product": "Payment succeeded. {credits} long-term {tier} paper credits were added. Start the paper again.",
611
  "quota_exhausted": "Your daily free quota and long-term quota are exhausted. Purchase long-term quota with Alipay.",
612
  "payment_create": "Create Alipay QR code",
613
  "payment_creating": "Creating the Alipay order…",
 
815
  "logout_short": "Выйти",
816
  "payment_dialog_title": "Оплата через Alipay",
817
  "payment_notice": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Оплата 9,9 юаня добавляет квоту на три статьи без срока действия.",
818
+ "product_entitlement_required": "Квота выбранного тарифа исчерпана. Приобретите квоту этого тарифа через Alipay.",
819
+ "payment_notice_product": "Покупка тарифа {tier}: оплата {amount} {currency} добавит {credits} долгосрочных квот на статьи.",
820
+ "payment_paid_product": "Оплата прошла успешно. Добавлено {credits} долгосрочных квот тарифа {tier}. Запустите создание статьи повторно.",
821
  "quota_exhausted": "Ежедневная бесплатная и долгосрочная квоты исчерпаны. Приобретите долгосрочную квоту через Alipay.",
822
  "payment_create": "Создать QR-код для оплаты",
823
  "payment_creating": "Создаётся заказ Alipay…",
 
1025
  "logout_short": "Шығу",
1026
  "payment_dialog_title": "Alipay арқылы төлеу",
1027
  "payment_notice": "Күнделікті тегін және ұзақ мерзімді квота таусылды. 9,9 юань төлегенде мерзімі шектелмеген үш мақала квотасы қосылады.",
1028
+ "product_entitlement_required": "Таңдалған нұсқаның квотасы таусылды. Осы нұсқаға арналған квотаны Alipay арқылы сатып алыңыз.",
1029
+ "payment_notice_product": "{tier} нұсқасын сатып алу: {amount} {currency} төлемі {credits} ұзақ мерзімді мақала квотасын қосады.",
1030
+ "payment_paid_product": "Төлем сәтті өтті. {tier} нұсқасына {credits} ұзақ мерзімді мақала квотасы қосылды. Мақала жасауды қайта бастаңыз.",
1031
  "quota_exhausted": "Күнделікті тегін және ұзақ мерзімді квота таусылды. Alipay арқылы ұзақ мерзімді квота сатып алыңыз.",
1032
  "payment_create": "Төлемге арналған QR-код жасау",
1033
  "payment_creating": "Alipay тапсырысы жасалуда…",
managed_auth.py CHANGED
@@ -135,7 +135,7 @@ def _clear_generation_session_state() -> None:
135
  "is_running", "start_pipeline", "resume_pipeline", "run_mode",
136
  "rag_engine", "up_amj_ref", "up_amj_data", "up_asq_ref",
137
  "up_asq_data", "sp_payment_required", "sp_payment_order",
138
- "sp_payment_request_id",
139
  ):
140
  st.session_state.pop(key, None)
141
 
@@ -261,6 +261,11 @@ def _normalize_product_ui_projection(value: Any) -> dict[str, Any]:
261
  raise RuntimeError("product_ui_projection_invalid_word_policy") from exc
262
  if minimum <= 0 or maximum < minimum:
263
  raise RuntimeError("product_ui_projection_invalid_word_policy")
 
 
 
 
 
264
  products[tier] = {
265
  "product_tier": tier,
266
  "product_policy_version": str(raw.get("product_policy_version") or ""),
@@ -269,13 +274,21 @@ def _normalize_product_ui_projection(value: Any) -> dict[str, Any]:
269
  "min_body_words": minimum,
270
  "max_body_words": maximum,
271
  "counting_contract_version": str(raw.get("counting_contract_version") or ""),
 
 
 
 
 
272
  }
273
  if not all((
274
  products[tier]["product_policy_version"],
275
  products[tier]["word_count_policy_version"],
276
  products[tier]["body_metric"],
277
  products[tier]["counting_contract_version"],
278
- )):
 
 
 
279
  raise RuntimeError("product_ui_projection_incomplete")
280
 
281
  try:
@@ -454,12 +467,19 @@ def logout_alipay_session(on_logout: Callable[[str, str], None] | None = None) -
454
  _clear_generation_session_state()
455
 
456
 
457
- def create_payment_order(idempotency_key: str) -> dict[str, Any]:
 
 
 
458
  normalized = str(idempotency_key or "").strip()
 
 
 
 
459
  result = _edge_call(
460
  "pay_create",
461
  normalized,
462
- {"idempotency_key": normalized},
463
  key_prefix="sp_pay_create",
464
  )
465
  if result is None:
@@ -493,22 +513,65 @@ def _qr_png_bytes(value: str) -> bytes:
493
 
494
 
495
  def _render_payment_purchase_body(language: str) -> None:
496
- st.warning(tr(language, "payment_notice"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
 
498
  if st.button(tr(language, "payment_create"), key="sp_create_payment_order"):
499
  st.session_state["sp_payment_request_id"] = (
500
  f"pay_{int(time.time() * 1000)}_{secrets.token_urlsafe(12)}"
501
  )
 
502
 
503
  request_id = str(st.session_state.get("sp_payment_request_id") or "").strip()
504
  order = st.session_state.get("sp_payment_order")
505
  if request_id and not order:
506
  st.info(tr(language, "payment_creating"))
507
- order_result = create_payment_order(request_id)
508
  order = dict(order_result.get("order") or {})
509
  if not order.get("merchant_order_no") or not order.get("qr_code"):
510
  raise RuntimeError("payment_order_missing_qr_code")
 
 
511
  st.session_state["sp_payment_order"] = order
 
 
 
512
 
513
  order = st.session_state.get("sp_payment_order") or {}
514
  qr_code = str(order.get("qr_code") or "").strip()
@@ -519,6 +582,9 @@ def _render_payment_purchase_body(language: str) -> None:
519
  status_result = payment_order_status(order.get("merchant_order_no", ""))
520
  updated = dict(status_result.get("order") or {})
521
  st.session_state["sp_payment_order"] = updated
 
 
 
522
  if updated.get("status") == "paid" and updated.get("credit_applied"):
523
  user = status_result.get("user")
524
  if isinstance(user, dict):
@@ -529,10 +595,23 @@ def _render_payment_purchase_body(language: str) -> None:
529
  st.session_state["sp_payment_required"] = False
530
  st.session_state["sp_payment_order"] = None
531
  st.session_state["sp_payment_request_id"] = ""
532
- st.success(tr(language, "payment_paid"))
 
 
 
 
 
 
 
 
 
 
 
 
533
  elif updated.get("status") in {"closed", "failed"}:
534
  st.session_state["sp_payment_order"] = None
535
  st.session_state["sp_payment_request_id"] = ""
 
536
  st.warning(tr(language, "payment_expired"))
537
 
538
 
 
135
  "is_running", "start_pipeline", "resume_pipeline", "run_mode",
136
  "rag_engine", "up_amj_ref", "up_amj_data", "up_asq_ref",
137
  "up_asq_data", "sp_payment_required", "sp_payment_order",
138
+ "sp_payment_request_id", "sp_payment_package_sku",
139
  ):
140
  st.session_state.pop(key, None)
141
 
 
261
  raise RuntimeError("product_ui_projection_invalid_word_policy") from exc
262
  if minimum <= 0 or maximum < minimum:
263
  raise RuntimeError("product_ui_projection_invalid_word_policy")
264
+ try:
265
+ amount_fen = int(raw.get("amount_fen"))
266
+ credits_granted = int(raw.get("credits_granted"))
267
+ except (TypeError, ValueError) as exc:
268
+ raise RuntimeError("product_ui_projection_invalid_billing_policy") from exc
269
  products[tier] = {
270
  "product_tier": tier,
271
  "product_policy_version": str(raw.get("product_policy_version") or ""),
 
274
  "min_body_words": minimum,
275
  "max_body_words": maximum,
276
  "counting_contract_version": str(raw.get("counting_contract_version") or ""),
277
+ "package_sku": str(raw.get("package_sku") or ""),
278
+ "billing_policy_version": str(raw.get("billing_policy_version") or ""),
279
+ "amount_fen": amount_fen,
280
+ "currency": str(raw.get("currency") or ""),
281
+ "credits_granted": credits_granted,
282
  }
283
  if not all((
284
  products[tier]["product_policy_version"],
285
  products[tier]["word_count_policy_version"],
286
  products[tier]["body_metric"],
287
  products[tier]["counting_contract_version"],
288
+ products[tier]["package_sku"],
289
+ products[tier]["billing_policy_version"],
290
+ products[tier]["currency"],
291
+ )) or amount_fen <= 0 or credits_granted <= 0:
292
  raise RuntimeError("product_ui_projection_incomplete")
293
 
294
  try:
 
467
  _clear_generation_session_state()
468
 
469
 
470
+ def create_payment_order(
471
+ idempotency_key: str,
472
+ package_sku: str | None = None,
473
+ ) -> dict[str, Any]:
474
  normalized = str(idempotency_key or "").strip()
475
+ payload: dict[str, Any] = {"idempotency_key": normalized}
476
+ normalized_sku = str(package_sku or "").strip()
477
+ if normalized_sku:
478
+ payload["package_sku"] = normalized_sku
479
  result = _edge_call(
480
  "pay_create",
481
  normalized,
482
+ payload,
483
  key_prefix="sp_pay_create",
484
  )
485
  if result is None:
 
513
 
514
 
515
  def _render_payment_purchase_body(language: str) -> None:
516
+ projection = get_product_ui_projection()
517
+ product_mode = projection.get("mode") == "a5_product"
518
+ selected_product: dict[str, Any] | None = None
519
+ package_sku = ""
520
+ if product_mode:
521
+ selected_tier = str(st.session_state.get("sp_product_tier") or "STANDARD").strip().upper()
522
+ raw_product = (projection.get("products") or {}).get(selected_tier)
523
+ if not isinstance(raw_product, dict):
524
+ raise RuntimeError("product_payment_projection_missing_tier")
525
+ selected_product = dict(raw_product)
526
+ package_sku = str(selected_product.get("package_sku") or "").strip()
527
+ amount_fen = int(selected_product.get("amount_fen") or 0)
528
+ credits = int(selected_product.get("credits_granted") or 0)
529
+ if not package_sku or amount_fen <= 0 or credits <= 0:
530
+ raise RuntimeError("product_payment_projection_invalid")
531
+ amount_text = f"{amount_fen / 100:.2f}".rstrip("0").rstrip(".")
532
+ currency = str(selected_product.get("currency") or "").strip()
533
+ if not currency:
534
+ raise RuntimeError("product_payment_projection_invalid_currency")
535
+ tier_name = tr(language, f"product_tier_{selected_tier.lower()}")
536
+ st.warning(
537
+ tr(
538
+ language,
539
+ "payment_notice_product",
540
+ tier=tier_name,
541
+ amount=amount_text,
542
+ currency=currency,
543
+ credits=credits,
544
+ )
545
+ )
546
+ existing_order = st.session_state.get("sp_payment_order")
547
+ existing_sku = str(st.session_state.get("sp_payment_package_sku") or "").strip()
548
+ order_sku = str((existing_order or {}).get("package_sku") or "").strip() if isinstance(existing_order, dict) else ""
549
+ if (existing_sku and existing_sku != package_sku) or (existing_order and order_sku != package_sku):
550
+ st.session_state["sp_payment_order"] = None
551
+ st.session_state["sp_payment_request_id"] = ""
552
+ else:
553
+ st.warning(tr(language, "payment_notice"))
554
 
555
  if st.button(tr(language, "payment_create"), key="sp_create_payment_order"):
556
  st.session_state["sp_payment_request_id"] = (
557
  f"pay_{int(time.time() * 1000)}_{secrets.token_urlsafe(12)}"
558
  )
559
+ st.session_state["sp_payment_package_sku"] = package_sku
560
 
561
  request_id = str(st.session_state.get("sp_payment_request_id") or "").strip()
562
  order = st.session_state.get("sp_payment_order")
563
  if request_id and not order:
564
  st.info(tr(language, "payment_creating"))
565
+ order_result = create_payment_order(request_id, package_sku or None)
566
  order = dict(order_result.get("order") or {})
567
  if not order.get("merchant_order_no") or not order.get("qr_code"):
568
  raise RuntimeError("payment_order_missing_qr_code")
569
+ if product_mode and str(order.get("package_sku") or "") != package_sku:
570
+ raise RuntimeError("payment_order_package_mismatch")
571
  st.session_state["sp_payment_order"] = order
572
+ projected = order_result.get("product_ui")
573
+ if isinstance(projected, dict):
574
+ st.session_state["sp_product_ui_projection"] = _normalize_product_ui_projection(projected)
575
 
576
  order = st.session_state.get("sp_payment_order") or {}
577
  qr_code = str(order.get("qr_code") or "").strip()
 
582
  status_result = payment_order_status(order.get("merchant_order_no", ""))
583
  updated = dict(status_result.get("order") or {})
584
  st.session_state["sp_payment_order"] = updated
585
+ projected = status_result.get("product_ui")
586
+ if isinstance(projected, dict):
587
+ st.session_state["sp_product_ui_projection"] = _normalize_product_ui_projection(projected)
588
  if updated.get("status") == "paid" and updated.get("credit_applied"):
589
  user = status_result.get("user")
590
  if isinstance(user, dict):
 
595
  st.session_state["sp_payment_required"] = False
596
  st.session_state["sp_payment_order"] = None
597
  st.session_state["sp_payment_request_id"] = ""
598
+ st.session_state["sp_payment_package_sku"] = ""
599
+ if product_mode and selected_product is not None:
600
+ tier_name = tr(language, f"product_tier_{str(selected_product.get('product_tier') or '').lower()}")
601
+ st.success(
602
+ tr(
603
+ language,
604
+ "payment_paid_product",
605
+ tier=tier_name,
606
+ credits=int(selected_product.get("credits_granted") or 0),
607
+ )
608
+ )
609
+ else:
610
+ st.success(tr(language, "payment_paid"))
611
  elif updated.get("status") in {"closed", "failed"}:
612
  st.session_state["sp_payment_order"] = None
613
  st.session_state["sp_payment_request_id"] = ""
614
+ st.session_state["sp_payment_package_sku"] = ""
615
  st.warning(tr(language, "payment_expired"))
616
 
617