refactor: switch to free-trial/private-deploy/buyout pricing, fix feature gaps, add SEO landing + deploy config
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
"""add enterprise_leads table
|
||||
|
||||
Revision ID: add_enterprise_leads
|
||||
Revises: add_perf_indexes
|
||||
Create Date: 2026-07-11
|
||||
|
||||
Stores inbound leads from the private-deployment / source-buyout CTAs.
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'add_enterprise_leads'
|
||||
down_revision = 'add_perf_indexes'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'enterprise_leads',
|
||||
sa.Column('id', sa.String(36), primary_key=True),
|
||||
sa.Column('type', sa.String(20), nullable=False, server_default='private'),
|
||||
sa.Column('name', sa.String(100), nullable=False, server_default=''),
|
||||
sa.Column('company', sa.String(200), nullable=False, server_default=''),
|
||||
sa.Column('phone', sa.String(50), nullable=False, server_default=''),
|
||||
sa.Column('email', sa.String(200), nullable=False, server_default=''),
|
||||
sa.Column('message', sa.Text(), nullable=False, server_default=''),
|
||||
sa.Column('status', sa.String(20), nullable=False, server_default='new'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
op.create_index('ix_enterprise_leads_type', 'enterprise_leads', ['type'])
|
||||
op.create_index('ix_enterprise_leads_status', 'enterprise_leads', ['status'])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('ix_enterprise_leads_status', table_name='enterprise_leads')
|
||||
op.drop_index('ix_enterprise_leads_type', table_name='enterprise_leads')
|
||||
op.drop_table('enterprise_leads')
|
||||
@@ -0,0 +1,34 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from app.database import get_db
|
||||
from app.models.enterprise_lead import EnterpriseLead
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class LeadCreate(BaseModel):
|
||||
type: str # private | buyout
|
||||
name: str
|
||||
company: str = ""
|
||||
phone: str = ""
|
||||
email: str = ""
|
||||
message: str = ""
|
||||
|
||||
|
||||
@router.post("")
|
||||
async def create_lead(payload: LeadCreate, db: AsyncSession = Depends(get_db)):
|
||||
if payload.type not in ("private", "buyout"):
|
||||
raise HTTPException(status_code=400, detail="Invalid lead type")
|
||||
lead = EnterpriseLead(
|
||||
type=payload.type,
|
||||
name=payload.name,
|
||||
company=payload.company,
|
||||
phone=payload.phone,
|
||||
email=payload.email,
|
||||
message=payload.message,
|
||||
)
|
||||
db.add(lead)
|
||||
await db.flush()
|
||||
await db.refresh(lead)
|
||||
return {"id": lead.id, "type": lead.type, "status": lead.status}
|
||||
@@ -26,6 +26,7 @@ CSRF_SKIP_ENDPOINTS = [
|
||||
"/api/v1/payment/",
|
||||
"/api/v1/whatsapp/webhook",
|
||||
"/api/v1/ai/",
|
||||
"/api/v1/leads",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -129,7 +129,7 @@ async def health():
|
||||
return {"status": "ok", "app": settings.APP_NAME, "version": "1.0.0"}
|
||||
|
||||
|
||||
from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits, agent
|
||||
from app.api.v1 import auth, marketing, translate, customer, quotation, whatsapp, product, exchange, push, admin, analytics, teams, onboarding, notification, feedback, payment, interaction, silent_pattern, training, followup, ai_assistant, discovery, discovery_record, certification, invoice, usage, referral, admin_search, search, admin_ai, credits, admin_credits, agent, leads
|
||||
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"])
|
||||
app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"])
|
||||
@@ -165,6 +165,7 @@ app.include_router(admin_credits.router, prefix="/api/v1/admin", tags=["admin"])
|
||||
app.include_router(credits.router, prefix="/api/v1/credits", tags=["credits"])
|
||||
app.include_router(search.router, prefix="/api/v1/search", tags=["search"])
|
||||
app.include_router(agent.router, prefix="/api/v1/agent", tags=["agent"])
|
||||
app.include_router(leads.router, prefix="/api/v1/leads", tags=["leads"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -24,6 +24,7 @@ from .user_credit import UserCredit
|
||||
from .credit_consumption import CreditConsumption
|
||||
from .credit_purchase import CreditPurchase
|
||||
from .agent_pipeline import AgentPipeline
|
||||
from .enterprise_lead import EnterpriseLead
|
||||
|
||||
__all__ = [
|
||||
"User", "Product",
|
||||
@@ -47,4 +48,5 @@ __all__ = [
|
||||
"CreditConsumption",
|
||||
"CreditPurchase",
|
||||
"AgentPipeline",
|
||||
"EnterpriseLead",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, String, Text, DateTime, func
|
||||
from app.database import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class EnterpriseLead(Base):
|
||||
__tablename__ = "enterprise_leads"
|
||||
|
||||
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
type = Column(String(20), nullable=False, default="private") # private | buyout
|
||||
name = Column(String(100), nullable=False, default="")
|
||||
company = Column(String(200), nullable=False, default="")
|
||||
phone = Column(String(50), nullable=False, default="")
|
||||
email = Column(String(200), nullable=False, default="")
|
||||
message = Column(Text, nullable=False, default="")
|
||||
status = Column(String(20), nullable=False, default="new") # new | contacted | done
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
|
||||
class TestLeadsAPI:
|
||||
async def test_create_private_lead(self, client: AsyncClient):
|
||||
res = await client.post(
|
||||
"/api/v1/leads",
|
||||
json={
|
||||
"type": "private",
|
||||
"name": "张三",
|
||||
"company": "测试外贸公司",
|
||||
"phone": "13800138000",
|
||||
"message": "想私有化部署",
|
||||
},
|
||||
)
|
||||
assert res.status_code == 200
|
||||
data = res.json()
|
||||
assert data["type"] == "private"
|
||||
assert data["status"] == "new"
|
||||
assert data["id"]
|
||||
|
||||
async def test_create_buyout_lead(self, client: AsyncClient):
|
||||
res = await client.post(
|
||||
"/api/v1/leads",
|
||||
json={"type": "buyout", "name": "李四", "phone": "13900139000"},
|
||||
)
|
||||
assert res.status_code == 200
|
||||
assert res.json()["type"] == "buyout"
|
||||
|
||||
async def test_invalid_type_rejected(self, client: AsyncClient):
|
||||
res = await client.post(
|
||||
"/api/v1/leads",
|
||||
json={"type": "wrong", "name": "x", "phone": "1"},
|
||||
)
|
||||
assert res.status_code == 400
|
||||
Reference in New Issue
Block a user