mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 07:19:35 +00:00
Close 6 critical launch gates for Primitive Launch Completion:
- DLQ (Dead Letter Queue): Redis-backed failure capture with retry drain
and admin endpoints (/admin/dlq/queues, /admin/dlq/{queue}/purge)
- PostHog client: zero-dependency HTTP funnel tracker with 16 event types
(landing_view → deal_won → payment_succeeded)
- Circuit breaker: in-memory fault isolation for external integrations
with registry and admin status endpoint (/admin/circuit-breakers)
- Pricing router: 3-tier plans (Starter 990/Growth 2490/Enterprise custom)
with Moyasar invoice checkout and webhook handler
- Config: added POSTHOG_API_KEY, MOYASAR_SECRET_KEY, DLQ settings
- Wiring: PostHog + DLQ initialized in main.py lifespan, pricing router
in API router
- RUNBOOK.md: 5 incident scenarios (service down, DB down, LLM down,
DB restore, version rollback)
- LAUNCH_GATES.md: 33-gate checklist across 7 categories
- 20 tests: all passing (DLQ 7, PostHog 4, circuit breaker 5, pricing 4)
https://claude.ai/code/session_01W1rJthWDkasijTdXCfxVHs
227 lines
7.5 KiB
Python
227 lines
7.5 KiB
Python
"""Pricing & Checkout — plans catalog + Moyasar invoice creation.
|
|
|
|
P0 for launch: exposes pricing plans and creates payment links
|
|
so the first real transaction can happen.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import hmac
|
|
import logging
|
|
import time
|
|
from typing import Any, Dict, List, Optional
|
|
from uuid import uuid4
|
|
|
|
from fastapi import APIRouter, HTTPException, Header, Request
|
|
from pydantic import BaseModel
|
|
|
|
logger = logging.getLogger("dealix.pricing")
|
|
|
|
router = APIRouter(prefix="/pricing", tags=["Pricing & Checkout"])
|
|
|
|
PLANS: List[Dict[str, Any]] = [
|
|
{
|
|
"id": "starter",
|
|
"name_en": "Starter",
|
|
"name_ar": "المبتدئ",
|
|
"price_sar": 990,
|
|
"billing": "monthly",
|
|
"features_en": [
|
|
"Up to 500 leads/month",
|
|
"AI lead scoring",
|
|
"WhatsApp outreach (100 msgs/day)",
|
|
"Basic CRM sync",
|
|
"Email support",
|
|
],
|
|
"features_ar": [
|
|
"حتى 500 عميل محتمل/شهر",
|
|
"تقييم العملاء بالذكاء الاصطناعي",
|
|
"تواصل واتساب (100 رسالة/يوم)",
|
|
"ربط CRM أساسي",
|
|
"دعم بالبريد الإلكتروني",
|
|
],
|
|
},
|
|
{
|
|
"id": "growth",
|
|
"name_en": "Growth",
|
|
"name_ar": "النمو",
|
|
"price_sar": 2490,
|
|
"billing": "monthly",
|
|
"features_en": [
|
|
"Up to 2,000 leads/month",
|
|
"AI lead scoring + enrichment",
|
|
"WhatsApp + Email outreach (500 msgs/day)",
|
|
"Full CRM two-way sync",
|
|
"Calendly booking integration",
|
|
"Approval workflows",
|
|
"Priority support",
|
|
],
|
|
"features_ar": [
|
|
"حتى 2,000 عميل محتمل/شهر",
|
|
"تقييم + إثراء العملاء بالذكاء الاصطناعي",
|
|
"تواصل واتساب + بريد (500 رسالة/يوم)",
|
|
"ربط CRM ثنائي الاتجاه",
|
|
"ربط حجز المواعيد",
|
|
"سير عمل الموافقات",
|
|
"دعم أولوية",
|
|
],
|
|
},
|
|
{
|
|
"id": "enterprise",
|
|
"name_en": "Enterprise",
|
|
"name_ar": "المؤسسات",
|
|
"price_sar": 0,
|
|
"billing": "custom",
|
|
"features_en": [
|
|
"Unlimited leads",
|
|
"Full AI agent suite",
|
|
"Dedicated success manager",
|
|
"Custom integrations",
|
|
"SLA guarantees",
|
|
"On-premise option",
|
|
],
|
|
"features_ar": [
|
|
"عملاء محتملون بلا حدود",
|
|
"مجموعة وكلاء ذكاء اصطناعي كاملة",
|
|
"مدير نجاح مخصص",
|
|
"تكاملات مخصصة",
|
|
"ضمانات مستوى الخدمة",
|
|
"خيار التثبيت المحلي",
|
|
],
|
|
},
|
|
]
|
|
|
|
|
|
@router.get("/plans")
|
|
async def list_plans() -> Dict[str, Any]:
|
|
return {"plans": PLANS, "currency": "SAR"}
|
|
|
|
|
|
@router.get("/plans/{plan_id}")
|
|
async def get_plan(plan_id: str) -> Dict[str, Any]:
|
|
for plan in PLANS:
|
|
if plan["id"] == plan_id:
|
|
return {"plan": plan, "currency": "SAR"}
|
|
raise HTTPException(status_code=404, detail=f"Plan {plan_id} not found")
|
|
|
|
|
|
class CheckoutRequest(BaseModel):
|
|
plan_id: str
|
|
customer_name: str
|
|
customer_email: str
|
|
customer_phone: str = ""
|
|
tenant_id: str = ""
|
|
locale: str = "ar"
|
|
|
|
|
|
@router.post("/checkout")
|
|
async def create_checkout(req: CheckoutRequest) -> Dict[str, Any]:
|
|
plan = next((p for p in PLANS if p["id"] == req.plan_id), None)
|
|
if not plan:
|
|
raise HTTPException(status_code=404, detail="Plan not found")
|
|
if plan["price_sar"] == 0:
|
|
return {
|
|
"status": "contact_sales",
|
|
"message_ar": "تواصل معنا للحصول على عرض مخصص",
|
|
"message_en": "Contact us for a custom quote",
|
|
}
|
|
|
|
from app.config import get_settings
|
|
settings = get_settings()
|
|
moyasar_key = getattr(settings, "MOYASAR_SECRET_KEY", "")
|
|
|
|
if not moyasar_key:
|
|
return {
|
|
"status": "checkout_unavailable",
|
|
"message": "Payment gateway not configured. Contact support.",
|
|
}
|
|
|
|
try:
|
|
import httpx
|
|
invoice_payload = {
|
|
"amount": plan["price_sar"] * 100,
|
|
"currency": "SAR",
|
|
"description": f"Dealix {plan['name_en']} - Monthly",
|
|
"metadata": {
|
|
"plan_id": plan["id"],
|
|
"tenant_id": req.tenant_id,
|
|
"customer_email": req.customer_email,
|
|
},
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
resp = await client.post(
|
|
"https://api.moyasar.com/v1/invoices",
|
|
json=invoice_payload,
|
|
auth=(moyasar_key, ""),
|
|
)
|
|
|
|
if resp.status_code in (200, 201):
|
|
data = resp.json()
|
|
return {
|
|
"status": "invoice_created",
|
|
"invoice_id": data.get("id"),
|
|
"payment_url": data.get("url"),
|
|
"amount_sar": plan["price_sar"],
|
|
"plan": plan["id"],
|
|
}
|
|
logger.error("Moyasar error: %d %s", resp.status_code, resp.text[:500])
|
|
raise HTTPException(status_code=502, detail="Payment gateway error")
|
|
except httpx.HTTPError as exc:
|
|
logger.error("Moyasar connection error: %s", exc)
|
|
raise HTTPException(status_code=502, detail="Payment gateway unreachable")
|
|
|
|
|
|
@router.post("/webhooks/moyasar")
|
|
async def moyasar_payment_webhook(
|
|
request: Request,
|
|
x_moyasar_signature: Optional[str] = Header(None, alias="X-Moyasar-Signature"),
|
|
) -> Dict[str, Any]:
|
|
body = await request.body()
|
|
payload = await request.json()
|
|
|
|
from app.config import get_settings
|
|
settings = get_settings()
|
|
webhook_secret = getattr(settings, "MOYASAR_WEBHOOK_SECRET", "")
|
|
|
|
if webhook_secret and x_moyasar_signature:
|
|
expected = hmac.new(
|
|
webhook_secret.encode(), body, hashlib.sha256
|
|
).hexdigest()
|
|
if not hmac.compare_digest(expected, x_moyasar_signature):
|
|
logger.warning("Moyasar webhook signature mismatch")
|
|
raise HTTPException(status_code=401, detail="Invalid signature")
|
|
|
|
event_type = payload.get("type", "")
|
|
data = payload.get("data", {})
|
|
|
|
from app.services.posthog_client import get_posthog, FunnelEvent
|
|
posthog = get_posthog()
|
|
|
|
if event_type == "payment_paid":
|
|
metadata = data.get("metadata", {})
|
|
await posthog.capture(
|
|
distinct_id=metadata.get("customer_email", "unknown"),
|
|
event=FunnelEvent.PAYMENT_SUCCEEDED,
|
|
properties={
|
|
"plan_id": metadata.get("plan_id"),
|
|
"amount_sar": data.get("amount", 0) / 100,
|
|
"invoice_id": data.get("invoice_id"),
|
|
},
|
|
)
|
|
logger.info("Payment succeeded: invoice=%s", data.get("invoice_id"))
|
|
return {"status": "processed", "event": event_type}
|
|
|
|
if event_type == "payment_failed":
|
|
metadata = data.get("metadata", {})
|
|
await posthog.capture(
|
|
distinct_id=metadata.get("customer_email", "unknown"),
|
|
event=FunnelEvent.PAYMENT_FAILED,
|
|
properties={"plan_id": metadata.get("plan_id")},
|
|
)
|
|
logger.warning("Payment failed: invoice=%s", data.get("invoice_id"))
|
|
return {"status": "processed", "event": event_type}
|
|
|
|
return {"status": "ignored", "event": event_type}
|