Improved features, implemented moco integration

This commit is contained in:
2026-02-18 19:59:14 +01:00
parent af2c9e7fd8
commit 8e4f155a16
11 changed files with 827 additions and 427 deletions

View File

@@ -11,7 +11,7 @@ from src.database.models import (
LinkedInProfile, LinkedInPost, Topic,
ProfileAnalysis, ResearchResult, GeneratedPost, PostType,
User, Profile, Company, Invitation, ExamplePost, ReferenceProfile,
ApiUsageLog, LicenseKey, CompanyDailyQuota
ApiUsageLog, LicenseKey, CompanyDailyQuota, LicenseKeyOffer
)
@@ -1252,6 +1252,59 @@ class DatabaseClient:
)
logger.info(f"Deleted license key: {key_id}")
# ==================== LICENSE KEY OFFERS ====================
async def create_license_key_offer(
self,
license_key_id: UUID,
moco_offer_id: int,
moco_offer_identifier: Optional[str],
moco_offer_url: Optional[str],
offer_title: Optional[str],
company_name: Optional[str],
price: Optional[float],
payment_frequency: Optional[str],
) -> "LicenseKeyOffer":
"""Save a MOCO offer linked to a license key."""
data = {
"license_key_id": str(license_key_id),
"moco_offer_id": moco_offer_id,
"moco_offer_identifier": moco_offer_identifier,
"moco_offer_url": moco_offer_url,
"offer_title": offer_title,
"company_name": company_name,
"price": price,
"payment_frequency": payment_frequency,
"status": "draft",
}
result = await asyncio.to_thread(
lambda: self.client.table("license_key_offers").insert(data).execute()
)
return LicenseKeyOffer(**result.data[0])
async def list_license_key_offers(self, license_key_id: UUID) -> list["LicenseKeyOffer"]:
"""List all MOCO offers for a license key."""
result = await asyncio.to_thread(
lambda: self.client.table("license_key_offers")
.select("*")
.eq("license_key_id", str(license_key_id))
.order("created_at", desc=True)
.execute()
)
return [LicenseKeyOffer(**row) for row in result.data]
async def update_license_key_offer_status(
self, offer_id: UUID, status: str
) -> "LicenseKeyOffer":
"""Update the status of a stored offer."""
result = await asyncio.to_thread(
lambda: self.client.table("license_key_offers")
.update({"status": status})
.eq("id", str(offer_id))
.execute()
)
return LicenseKeyOffer(**result.data[0])
# ==================== COMPANY QUOTAS ====================
async def get_company_daily_quota(
@@ -1356,6 +1409,66 @@ class DatabaseClient:
return True, ""
# ==================== EMAIL ACTION TOKENS ====================
async def create_email_token(self, token: str, post_id: UUID, action: str, expires_hours: int = 72) -> None:
"""Store an email action token in the database."""
from datetime import timedelta
expires_at = datetime.now(timezone.utc) + timedelta(hours=expires_hours)
data = {
"token": token,
"post_id": str(post_id),
"action": action,
"expires_at": expires_at.isoformat(),
"used": False,
}
await asyncio.to_thread(
lambda: self.client.table("email_action_tokens").insert(data).execute()
)
logger.debug(f"Created email token for post {post_id} action={action}")
async def get_email_token(self, token: str) -> Optional[Dict[str, Any]]:
"""Retrieve email token data; returns None if not found."""
result = await asyncio.to_thread(
lambda: self.client.table("email_action_tokens").select("*").eq("token", token).execute()
)
if not result.data:
return None
return result.data[0]
async def mark_email_token_used(self, token: str) -> None:
"""Mark an email token as used."""
await asyncio.to_thread(
lambda: self.client.table("email_action_tokens").update({"used": True}).eq("token", token).execute()
)
async def cleanup_expired_email_tokens(self) -> None:
"""Delete expired email tokens from the database."""
now = datetime.now(timezone.utc).isoformat()
result = await asyncio.to_thread(
lambda: self.client.table("email_action_tokens").delete().lt("expires_at", now).execute()
)
count = len(result.data) if result.data else 0
if count:
logger.info(f"Cleaned up {count} expired email tokens")
# ==================== LINKEDIN TOKEN REFRESH ====================
async def get_expiring_linkedin_accounts(self, within_days: int = 7) -> list:
"""Return active LinkedIn accounts whose tokens expire within within_days and have a refresh_token."""
from src.database.models import LinkedInAccount
from datetime import timedelta
cutoff = (datetime.now(timezone.utc) + timedelta(days=within_days)).isoformat()
result = await asyncio.to_thread(
lambda: self.client.table("linkedin_accounts")
.select("*")
.eq("is_active", True)
.lt("token_expires_at", cutoff)
.not_.is_("refresh_token", "null")
.execute()
)
return [LinkedInAccount(**row) for row in result.data]
# Global database client instance
db = DatabaseClient()