diff --git a/backend/alembic/versions/0798c5c09c8c_add_discovery_records_table.py b/backend/alembic/versions/0798c5c09c8c_add_discovery_records_table.py new file mode 100644 index 0000000..dea2298 --- /dev/null +++ b/backend/alembic/versions/0798c5c09c8c_add_discovery_records_table.py @@ -0,0 +1,39 @@ +"""add discovery_records table + +Revision ID: 0798c5c09c8c +Revises: 7fe16f1f9962 +Create Date: 2026-05-27 15:54:21.092439 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +revision: str = '0798c5c09c8c' +down_revision: Union[str, None] = '7fe16f1f9962' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('discovery_records', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('product', sa.String(length=500), nullable=False), + sa.Column('market', sa.String(length=200), nullable=True), + sa.Column('companies', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_discovery_records_user_id'), 'discovery_records', ['user_id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_discovery_records_user_id'), table_name='discovery_records') + op.drop_table('discovery_records') + # ### end Alembic commands ### \ No newline at end of file diff --git a/backend/app/api/v1/discovery_record.py b/backend/app/api/v1/discovery_record.py new file mode 100644 index 0000000..3384d5a --- /dev/null +++ b/backend/app/api/v1/discovery_record.py @@ -0,0 +1,131 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, desc +from typing import List, Optional +from app.database import get_db +from app.api.v1.deps import get_current_user_id +from app.models.discovery_record import DiscoveryRecord +from pydantic import BaseModel +from datetime import datetime +import uuid + +router = APIRouter() + + +class SaveRecordRequest(BaseModel): + product: str + market: str = "" + companies: list + + +class RecordResponse(BaseModel): + id: str + product: str + market: str + companies: list + created_at: str + + +@router.post("/records") +async def save_record( + req: SaveRecordRequest, + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + record = DiscoveryRecord( + user_id=uuid.UUID(user_id), + product=req.product, + market=req.market, + companies=req.companies, + ) + db.add(record) + await db.commit() + return {"success": True, "data": {"id": str(record.id)}} + + +@router.get("/records") +async def list_records( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=50), + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + stmt = ( + select(DiscoveryRecord) + .where(DiscoveryRecord.user_id == uuid.UUID(user_id)) + .order_by(desc(DiscoveryRecord.created_at)) + .offset((page - 1) * size) + .limit(size) + ) + result = await db.execute(stmt) + records = result.scalars().all() + total_stmt = select(DiscoveryRecord).where(DiscoveryRecord.user_id == uuid.UUID(user_id)) + total_result = await db.execute(total_stmt) + total = len(total_result.scalars().all()) + return { + "items": [ + { + "id": str(r.id), + "product": r.product, + "market": r.market, + "companies": r.companies or [], + "created_at": r.created_at.isoformat() if r.created_at else "", + } + for r in records + ], + "total": total, + "page": page, + "size": size, + } + + +@router.get("/records/{record_id}") +async def get_record( + record_id: str, + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + try: + rid = uuid.UUID(record_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid record ID") + result = await db.execute( + select(DiscoveryRecord).where( + DiscoveryRecord.id == rid, + DiscoveryRecord.user_id == uuid.UUID(user_id), + ) + ) + r = result.scalar_one_or_none() + if not r: + raise HTTPException(status_code=404, detail="Record not found") + return { + "id": str(r.id), + "product": r.product, + "market": r.market, + "companies": r.companies or [], + "created_at": r.created_at.isoformat() if r.created_at else "", + } + + +@router.delete("/records/{record_id}") +async def delete_record( + record_id: str, + user_id: str = Depends(get_current_user_id), + db: AsyncSession = Depends(get_db), +): + try: + rid = uuid.UUID(record_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid record ID") + result = await db.execute( + select(DiscoveryRecord).where( + DiscoveryRecord.id == rid, + DiscoveryRecord.user_id == uuid.UUID(user_id), + ) + ) + r = result.scalar_one_or_none() + if not r: + raise HTTPException(status_code=404, detail="Record not found") + await db.delete(r) + await db.commit() + return {"success": True} diff --git a/backend/app/main.py b/backend/app/main.py index 15fffea..8bee3e2 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -54,7 +54,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, certification, invoice, usage, referral, admin_search, search +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 app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) app.include_router(marketing.router, prefix="/api/v1/marketing", tags=["marketing"]) @@ -79,6 +79,7 @@ app.include_router(training.router, prefix="/api/v1/training", tags=["training"] app.include_router(followup.router, prefix="/api/v1/followup", tags=["followup"]) app.include_router(ai_assistant.router, prefix="/api/v1/ai", tags=["ai-assistant"]) app.include_router(discovery.router, prefix="/api/v1/discovery", tags=["discovery"]) +app.include_router(discovery_record.router, prefix="/api/v1/discovery", tags=["discovery"]) app.include_router(certification.router, prefix="/api/v1/certification", tags=["certification"]) app.include_router(invoice.router, prefix="/api/v1/invoices", tags=["invoices"]) app.include_router(usage.router, prefix="/api/v1/usage", tags=["usage"]) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 24f6a68..d3b6c99 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -16,6 +16,7 @@ from .certification import Certification, CertType, CertStatus from .invoice import Invoice, InvoiceType, InvoiceStatus from .referral import ReferralCode, Referral from .search_provider import SearchProvider +from .discovery_record import DiscoveryRecord __all__ = [ "User", "Product", @@ -36,4 +37,5 @@ __all__ = [ "Invoice", "InvoiceType", "InvoiceStatus", "ReferralCode", "Referral", "SearchProvider", + "DiscoveryRecord", ] diff --git a/backend/app/models/discovery_record.py b/backend/app/models/discovery_record.py new file mode 100644 index 0000000..508bcfe --- /dev/null +++ b/backend/app/models/discovery_record.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, String, DateTime, Text +from sqlalchemy.dialects.postgresql import UUID, JSONB +from datetime import datetime +from app.database import Base +import uuid + + +class DiscoveryRecord(Base): + __tablename__ = "discovery_records" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), nullable=False, index=True) + product = Column(String(500), nullable=False) + market = Column(String(200), default="") + companies = Column(JSONB, default=[]) + created_at = Column(DateTime, default=datetime.utcnow) diff --git a/backend/app/services/discovery.py b/backend/app/services/discovery.py index 028b90c..eb8b080 100644 --- a/backend/app/services/discovery.py +++ b/backend/app/services/discovery.py @@ -125,7 +125,7 @@ URL: {company_url} async def _web_search_all(self, queries: list) -> dict: try: - results = await search_bing_batch(queries[:4], max_per_query=5) + results = await search_bing_batch(queries[:3], max_per_query=4) if results: return {"results": self._dedup_and_filter(results)[:15], "provider": "bing"} except Exception as e: diff --git a/user-frontend/src/api/index.js b/user-frontend/src/api/index.js index 9d66d2e..45c716f 100644 --- a/user-frontend/src/api/index.js +++ b/user-frontend/src/api/index.js @@ -71,9 +71,13 @@ export function generateMarketing(data) { return http.post('/marketing/generate' export function getKeywords(data) { return http.post('/marketing/keywords', data) } export function competitorAnalysis(data) { return http.post('/marketing/competitor-analysis', data) } -export function discoverySearch(data) { return http.post('/discovery/search', data) } -export function discoveryAnalyze(data) { return http.post('/discovery/analyze', data) } -export function discoveryOutreach(data) { return http.post('/discovery/outreach', data) } +export function discoverySearch(data) { return http.post('/discovery/search', data, { timeout: 120000 }) } +export function discoveryAnalyze(data) { return http.post('/discovery/analyze', data, { timeout: 60000 }) } +export function discoveryOutreach(data) { return http.post('/discovery/outreach', data, { timeout: 60000 }) } +export function saveDiscoveryRecord(data) { return http.post('/discovery/records', data) } +export function listDiscoveryRecords(params) { return http.get('/discovery/records', { params }) } +export function getDiscoveryRecord(id) { return http.get(`/discovery/records/${id}`) } +export function deleteDiscoveryRecord(id) { return http.delete(`/discovery/records/${id}`) } export function getFollowupStats() { return http.get('/followup/stats') } export function getFollowupPending() { return http.get('/followup/pending') } diff --git a/user-frontend/src/views/Discovery.vue b/user-frontend/src/views/Discovery.vue index 1cc332a..94d5e9f 100644 --- a/user-frontend/src/views/Discovery.vue +++ b/user-frontend/src/views/Discovery.vue @@ -1,6 +1,6 @@