Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/app/services/marketplace_service.py
2026-05-14 01:30:20 +03:30

980 lines
30 KiB
Python
Executable file

from __future__ import annotations
from datetime import datetime, timedelta, date
from decimal import Decimal
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy.orm import Session
from adapters.db.models.marketplace import (
MarketplacePlugin,
MarketplacePluginPlan,
MarketplaceOrder,
MarketplaceInvoice,
BusinessPlugin,
)
from adapters.db.models.wallet import WalletTransaction
from adapters.db.models.currency import Currency
from adapters.db.models.business import Business
from app.core.responses import ApiError
def _period_to_duration(period: str) -> Optional[timedelta]:
period = (period or "").lower()
if period == "monthly":
return timedelta(days=30)
if period == "yearly":
return timedelta(days=365)
if period == "lifetime":
return None
# پیش‌فرض: یک ماه
return timedelta(days=30)
def _build_invoice_code(db: Session) -> str:
today = datetime.utcnow().strftime("%Y%m%d")
# شمارنده ساده بر اساس تاریخ
base = f"MKP-{today}"
like_pattern = f"{base}-%"
last = (
db.query(MarketplaceInvoice)
.filter(MarketplaceInvoice.code.like(like_pattern))
.order_by(MarketplaceInvoice.id.desc())
.first()
)
next_num = 1
if last and isinstance(last.code, str) and last.code.startswith(base + "-"):
try:
next_num = int(last.code.split("-")[-1]) + 1
except Exception:
next_num = 1
return f"{base}-{next_num:04d}"
def list_plugins(db: Session) -> List[Dict[str, Any]]:
items = (
db.query(MarketplacePlugin)
.filter(MarketplacePlugin.is_active == True) # noqa: E712
.order_by(MarketplacePlugin.id.desc())
.all()
)
result: List[Dict[str, Any]] = []
for it in items:
plans = (
db.query(MarketplacePluginPlan)
.filter(
MarketplacePluginPlan.plugin_id == it.id,
MarketplacePluginPlan.is_active == True, # noqa: E712
)
.order_by(MarketplacePluginPlan.price.asc())
.all()
)
# دریافت اطلاعات ارز برای هر پلن
plans_data = []
for p in plans:
currency = db.query(Currency).filter(Currency.id == int(p.currency_id)).first()
plan_data = {
"id": p.id,
"period": p.period,
"price": float(p.price or 0),
"currency_id": p.currency_id,
"is_active": bool(p.is_active),
}
# اضافه کردن اطلاعات ارز
if currency:
plan_data["currency"] = {
"id": currency.id,
"code": currency.code,
"title": currency.title,
"symbol": currency.symbol,
}
plans_data.append(plan_data)
result.append(
{
"id": it.id,
"code": it.code,
"name": it.name,
"description": it.description,
"category": it.category,
"icon_url": it.icon_url,
"trial_days": it.trial_days,
"trial_allowed": it.trial_allowed,
"plans": plans_data,
}
)
return result
def purchase_plugin(
db: Session,
business_id: int,
user_id: int,
payload: Dict[str, Any],
) -> Dict[str, Any]:
"""
خرید افزونه برای کسب‌وکار با استفاده از transaction برای اطمینان از یکپارچگی داده‌ها
"""
try:
plugin_id = int(payload.get("plugin_id") or 0)
plan_id = int(payload.get("plan_id") or 0)
quantity = int(payload.get("quantity") or 1)
if quantity <= 0:
raise ApiError("INVALID_QUANTITY", "تعداد نامعتبر است", http_status=400)
# بررسی افزونه و پلن با lock برای جلوگیری از race condition
plugin = db.query(MarketplacePlugin).filter(
MarketplacePlugin.id == plugin_id,
MarketplacePlugin.is_active == True # noqa: E712
).with_for_update().first()
if not plugin:
raise ApiError("PLUGIN_NOT_FOUND", "افزونه یافت نشد یا غیرفعال است", http_status=404)
plan = db.query(MarketplacePluginPlan).filter(
MarketplacePluginPlan.id == plan_id,
MarketplacePluginPlan.plugin_id == plugin.id,
MarketplacePluginPlan.is_active == True # noqa: E712
).with_for_update().first()
if not plan:
raise ApiError("PLAN_NOT_FOUND", "پلن افزونه یافت نشد یا غیرفعال است", http_status=404)
# اعتبارسنجی ارز
currency = db.query(Currency).filter(Currency.id == int(plan.currency_id)).first()
if not currency:
raise ApiError("CURRENCY_NOT_FOUND", "ارز پلن نامعتبر است", http_status=400)
business = db.query(Business).filter(Business.id == int(business_id)).first()
if not business:
raise ApiError("BUSINESS_NOT_FOUND", "کسب‌وکار یافت نشد", http_status=404)
unit_price = Decimal(str(plan.price or 0))
total_price = unit_price * Decimal(quantity)
# ایجاد سفارش
order = MarketplaceOrder(
business_id=int(business_id),
plugin_id=int(plugin.id),
plan_id=int(plan.id),
quantity=quantity,
unit_price=unit_price,
total_price=total_price,
currency_id=plan.currency_id,
status="pending",
)
db.add(order)
db.flush()
# ایجاد صورتحساب بازار
invoice_code = _build_invoice_code(db)
invoice = MarketplaceInvoice(
order_id=order.id,
business_id=int(business_id),
code=invoice_code,
total=total_price,
currency_id=plan.currency_id,
status="issued",
issued_at=datetime.utcnow(),
)
db.add(invoice)
db.flush()
order.invoice_id = invoice.id
db.flush()
# تلاش برای پرداخت از کیف‌پول
# دریافت اتمی حساب کیف‌پول و بررسی مانده
from adapters.db.models.wallet import WalletAccount # import محلی برای جلوگیری از چرخه
account = (
db.query(WalletAccount)
.filter(WalletAccount.business_id == int(business_id))
.with_for_update()
.first()
)
if account is None:
# ایجاد خودکار حساب کیف‌پول
account = WalletAccount(
business_id=int(business_id),
available_balance=Decimal("0"),
pending_balance=Decimal("0"),
status="active"
)
db.add(account)
db.flush()
available = Decimal(str(account.available_balance or 0))
if available < total_price:
# عدم کفایت موجودی - rollback نمی‌کنیم چون order و invoice باید ثبت شوند
shortfall = float(total_price - available)
return {
"order_id": order.id,
"invoice_id": invoice.id,
"status": "insufficient_funds",
"required_amount": float(total_price),
"available_amount": float(available),
"shortfall": float(Decimal(str(shortfall))),
}
# کسر موجودی و ثبت تراکنش
account.available_balance = available - total_price
db.flush()
tx = WalletTransaction(
business_id=int(business_id),
type="plugin_purchase",
status="succeeded",
amount=total_price,
fee_amount=Decimal("0"),
description=f"خرید افزونه {plugin.name} - پلن {plan.period}",
external_ref=str(invoice.id),
document_id=None,
)
db.add(tx)
db.flush()
# تسویه سفارش و صورتحساب
invoice.status = "paid"
invoice.paid_at = datetime.utcnow()
order.status = "paid"
order.wallet_transaction_id = tx.id
db.flush()
# فعال‌سازی لایسنس
duration = _period_to_duration(plan.period)
starts_at = datetime.utcnow()
ends_at = (starts_at + duration) if duration is not None else None
# بررسی لایسنس موجود - اگر منقضی شده باشد، آن را تمدید می‌کنیم
existing = (
db.query(BusinessPlugin)
.filter(
BusinessPlugin.business_id == int(business_id),
BusinessPlugin.plugin_id == plugin.id
)
.with_for_update()
.first()
)
if existing:
# اگر لایسنس موجود است، آن را تمدید می‌کنیم
# اگر trial بود، آن را به خرید تبدیل می‌کنیم
existing.plan_id = plan.id
existing.status = "active"
existing.starts_at = starts_at
existing.ends_at = ends_at
existing.is_trial = False # تبدیل trial به خرید
existing.trial_started_at = None
else:
db.add(
BusinessPlugin(
business_id=int(business_id),
plugin_id=plugin.id,
plan_id=plan.id,
status="active",
starts_at=starts_at,
ends_at=ends_at,
auto_renew=False,
)
)
db.flush()
return {
"order_id": order.id,
"invoice_id": invoice.id,
"wallet_transaction_id": tx.id,
"status": "paid",
"license": {
"plugin_id": plugin.id,
"plan_id": plan.id,
"starts_at": starts_at,
"ends_at": ends_at,
"status": "active",
},
}
except Exception as e:
# در صورت خطا، rollback می‌کنیم
db.rollback()
# اگر ApiError است، آن را دوباره raise می‌کنیم
if isinstance(e, ApiError):
raise
# در غیر این صورت، آن را به عنوان خطای داخلی می‌فرستیم
raise ApiError("INTERNAL_ERROR", f"خطا در خرید افزونه: {str(e)}", http_status=500)
def list_orders(db: Session, business_id: int, limit: int = 20, skip: int = 0) -> Dict[str, Any]:
q = (
db.query(MarketplaceOrder)
.filter(MarketplaceOrder.business_id == int(business_id))
.order_by(MarketplaceOrder.id.desc())
)
total = q.count()
items = q.offset(max(0, int(skip))).limit(max(1, min(200, int(limit)))).all()
result_items = []
for it in items:
currency = db.query(Currency).filter(Currency.id == int(it.currency_id)).first()
item_data = {
"id": it.id,
"plugin_id": it.plugin_id,
"plan_id": it.plan_id,
"quantity": it.quantity,
"unit_price": float(it.unit_price or 0),
"total_price": float(it.total_price or 0),
"currency_id": it.currency_id,
"status": it.status,
"invoice_id": it.invoice_id,
"wallet_transaction_id": it.wallet_transaction_id,
"created_at": it.created_at,
}
# اضافه کردن اطلاعات ارز
if currency:
item_data["currency"] = {
"id": currency.id,
"code": currency.code,
"title": currency.title,
"symbol": currency.symbol,
}
result_items.append(item_data)
return {
"items": result_items,
"total": total,
"limit": limit,
"page": (skip // limit) + 1 if limit > 0 else 1,
}
def list_invoices(db: Session, business_id: int, limit: int = 20, skip: int = 0) -> Dict[str, Any]:
q = (
db.query(MarketplaceInvoice)
.filter(MarketplaceInvoice.business_id == int(business_id))
.order_by(MarketplaceInvoice.id.desc())
)
total = q.count()
items = q.offset(max(0, int(skip))).limit(max(1, min(200, int(limit)))).all()
result_items = []
for it in items:
currency = db.query(Currency).filter(Currency.id == int(it.currency_id)).first()
item_data = {
"id": it.id,
"code": it.code,
"total": float(it.total or 0),
"currency_id": it.currency_id,
"status": it.status,
"issued_at": it.issued_at,
"paid_at": it.paid_at,
"order_id": it.order_id,
}
# اضافه کردن اطلاعات ارز
if currency:
item_data["currency"] = {
"id": currency.id,
"code": currency.code,
"title": currency.title,
"symbol": currency.symbol,
}
result_items.append(item_data)
return {
"items": result_items,
"total": total,
"limit": limit,
"page": (skip // limit) + 1 if limit > 0 else 1,
}
# ========== Admin Functions ==========
def create_plugin(db: Session, payload: Dict[str, Any]) -> Dict[str, Any]:
code = str(payload.get("code") or "").strip()
name = str(payload.get("name") or "").strip()
description = payload.get("description")
category = payload.get("category")
icon_url = payload.get("icon_url")
is_active = bool(payload.get("is_active", True))
trial_days = payload.get("trial_days")
trial_allowed = bool(payload.get("trial_allowed", False))
if not code:
raise ApiError("INVALID_CODE", "کد افزونه الزامی است", http_status=400)
if not name:
raise ApiError("INVALID_NAME", "نام افزونه الزامی است", http_status=400)
# اعتبارسنجی trial_days
if trial_allowed:
if trial_days is None or int(trial_days or 0) <= 0:
raise ApiError("INVALID_TRIAL_DAYS", "تعداد روزهای trial باید بیشتر از صفر باشد", http_status=400)
trial_days = int(trial_days)
else:
trial_days = None
# بررسی تکراری نبودن کد
existing = db.query(MarketplacePlugin).filter(MarketplacePlugin.code == code).first()
if existing:
raise ApiError("DUPLICATE_CODE", f"افزونه با کد {code} قبلاً ثبت شده است", http_status=400)
plugin = MarketplacePlugin(
code=code,
name=name,
description=description,
category=category,
icon_url=icon_url,
is_active=is_active,
trial_days=trial_days,
trial_allowed=trial_allowed,
)
db.add(plugin)
db.commit()
db.refresh(plugin)
return {
"id": plugin.id,
"code": plugin.code,
"name": plugin.name,
"description": plugin.description,
"category": plugin.category,
"icon_url": plugin.icon_url,
"is_active": plugin.is_active,
"trial_days": plugin.trial_days,
"trial_allowed": plugin.trial_allowed,
"created_at": plugin.created_at,
"updated_at": plugin.updated_at,
}
def update_plugin(db: Session, plugin_id: int, payload: Dict[str, Any]) -> Dict[str, Any]:
plugin = db.query(MarketplacePlugin).filter(MarketplacePlugin.id == plugin_id).first()
if not plugin:
raise ApiError("PLUGIN_NOT_FOUND", "افزونه یافت نشد", http_status=404)
if "code" in payload:
new_code = str(payload.get("code") or "").strip()
if new_code and new_code != plugin.code:
existing = db.query(MarketplacePlugin).filter(MarketplacePlugin.code == new_code, MarketplacePlugin.id != plugin_id).first()
if existing:
raise ApiError("DUPLICATE_CODE", f"افزونه با کد {new_code} قبلاً ثبت شده است", http_status=400)
plugin.code = new_code
if "name" in payload:
name = str(payload.get("name") or "").strip()
if name:
plugin.name = name
if "description" in payload:
plugin.description = payload.get("description")
if "category" in payload:
plugin.category = payload.get("category")
if "icon_url" in payload:
plugin.icon_url = payload.get("icon_url")
if "is_active" in payload:
plugin.is_active = bool(payload.get("is_active"))
if "trial_allowed" in payload:
trial_allowed = bool(payload.get("trial_allowed", False))
if trial_allowed:
trial_days = payload.get("trial_days")
if trial_days is None or int(trial_days or 0) <= 0:
raise ApiError("INVALID_TRIAL_DAYS", "تعداد روزهای trial باید بیشتر از صفر باشد", http_status=400)
plugin.trial_days = int(trial_days)
plugin.trial_allowed = True
else:
plugin.trial_days = None
plugin.trial_allowed = False
elif "trial_days" in payload:
# اگر فقط trial_days تغییر کرده، بررسی می‌کنیم
trial_days = payload.get("trial_days")
if trial_days is not None and int(trial_days or 0) > 0:
plugin.trial_days = int(trial_days)
plugin.trial_allowed = True
elif trial_days is None or int(trial_days or 0) <= 0:
plugin.trial_days = None
plugin.trial_allowed = False
db.commit()
db.refresh(plugin)
return {
"id": plugin.id,
"code": plugin.code,
"name": plugin.name,
"description": plugin.description,
"category": plugin.category,
"icon_url": plugin.icon_url,
"is_active": plugin.is_active,
"trial_days": plugin.trial_days,
"trial_allowed": plugin.trial_allowed,
"created_at": plugin.created_at,
"updated_at": plugin.updated_at,
}
def delete_plugin(db: Session, plugin_id: int) -> Dict[str, Any]:
plugin = db.query(MarketplacePlugin).filter(MarketplacePlugin.id == plugin_id).first()
if not plugin:
raise ApiError("PLUGIN_NOT_FOUND", "افزونه یافت نشد", http_status=404)
# بررسی استفاده از افزونه
has_orders = db.query(MarketplaceOrder).filter(MarketplaceOrder.plugin_id == plugin_id).first()
has_licenses = db.query(BusinessPlugin).filter(BusinessPlugin.plugin_id == plugin_id).first()
if has_orders or has_licenses:
# غیرفعال کردن به جای حذف
plugin.is_active = False
db.commit()
return {"message": "افزونه غیرفعال شد (به دلیل استفاده در سفارش‌ها یا لایسنس‌ها قابل حذف نیست)"}
db.delete(plugin)
db.commit()
return {"message": "افزونه با موفقیت حذف شد"}
def create_plugin_plan(db: Session, plugin_id: int, payload: Dict[str, Any]) -> Dict[str, Any]:
plugin = db.query(MarketplacePlugin).filter(MarketplacePlugin.id == plugin_id).first()
if not plugin:
raise ApiError("PLUGIN_NOT_FOUND", "افزونه یافت نشد", http_status=404)
period = str(payload.get("period") or "").strip().lower()
price = Decimal(str(payload.get("price") or 0))
currency_id = int(payload.get("currency_id") or 0)
is_active = bool(payload.get("is_active", True))
if period not in ("monthly", "yearly", "lifetime"):
raise ApiError("INVALID_PERIOD", "دوره باید یکی از monthly, yearly, lifetime باشد", http_status=400)
if price < 0:
raise ApiError("INVALID_PRICE", "قیمت نمی‌تواند منفی باشد", http_status=400)
if not currency_id:
raise ApiError("INVALID_CURRENCY", "ارز الزامی است", http_status=400)
currency = db.query(Currency).filter(Currency.id == currency_id).first()
if not currency:
raise ApiError("CURRENCY_NOT_FOUND", "ارز یافت نشد", http_status=404)
plan = MarketplacePluginPlan(
plugin_id=plugin_id,
period=period,
price=price,
currency_id=currency_id,
is_active=is_active,
)
db.add(plan)
db.commit()
db.refresh(plan)
return {
"id": plan.id,
"plugin_id": plan.plugin_id,
"period": plan.period,
"price": float(plan.price or 0),
"currency_id": plan.currency_id,
"is_active": plan.is_active,
"created_at": plan.created_at,
"updated_at": plan.updated_at,
}
def update_plugin_plan(db: Session, plan_id: int, payload: Dict[str, Any]) -> Dict[str, Any]:
plan = db.query(MarketplacePluginPlan).filter(MarketplacePluginPlan.id == plan_id).first()
if not plan:
raise ApiError("PLAN_NOT_FOUND", "پلن یافت نشد", http_status=404)
if "period" in payload:
period = str(payload.get("period") or "").strip().lower()
if period not in ("monthly", "yearly", "lifetime"):
raise ApiError("INVALID_PERIOD", "دوره باید یکی از monthly, yearly, lifetime باشد", http_status=400)
plan.period = period
if "price" in payload:
price = Decimal(str(payload.get("price") or 0))
if price < 0:
raise ApiError("INVALID_PRICE", "قیمت نمی‌تواند منفی باشد", http_status=400)
plan.price = price
if "currency_id" in payload:
currency_id = int(payload.get("currency_id") or 0)
if currency_id:
currency = db.query(Currency).filter(Currency.id == currency_id).first()
if not currency:
raise ApiError("CURRENCY_NOT_FOUND", "ارز یافت نشد", http_status=404)
plan.currency_id = currency_id
if "is_active" in payload:
plan.is_active = bool(payload.get("is_active"))
db.commit()
db.refresh(plan)
return {
"id": plan.id,
"plugin_id": plan.plugin_id,
"period": plan.period,
"price": float(plan.price or 0),
"currency_id": plan.currency_id,
"is_active": plan.is_active,
"created_at": plan.created_at,
"updated_at": plan.updated_at,
}
def delete_plugin_plan(db: Session, plan_id: int) -> Dict[str, Any]:
plan = db.query(MarketplacePluginPlan).filter(MarketplacePluginPlan.id == plan_id).first()
if not plan:
raise ApiError("PLAN_NOT_FOUND", "پلن یافت نشد", http_status=404)
# بررسی استفاده از پلن
has_orders = db.query(MarketplaceOrder).filter(MarketplaceOrder.plan_id == plan_id).first()
has_licenses = db.query(BusinessPlugin).filter(BusinessPlugin.plan_id == plan_id).first()
if has_orders or has_licenses:
# غیرفعال کردن به جای حذف
plan.is_active = False
db.commit()
return {"message": "پلن غیرفعال شد (به دلیل استفاده در سفارش‌ها یا لایسنس‌ها قابل حذف نیست)"}
db.delete(plan)
db.commit()
return {"message": "پلن با موفقیت حذف شد"}
def list_all_plugins(db: Session, only_active: Optional[bool] = None) -> List[Dict[str, Any]]:
query = db.query(MarketplacePlugin)
if only_active is not None:
query = query.filter(MarketplacePlugin.is_active == only_active) # noqa: E712
items = query.order_by(MarketplacePlugin.id.desc()).all()
result: List[Dict[str, Any]] = []
for it in items:
plans = (
db.query(MarketplacePluginPlan)
.filter(MarketplacePluginPlan.plugin_id == it.id)
.order_by(MarketplacePluginPlan.price.asc())
.all()
)
result.append(
{
"id": it.id,
"code": it.code,
"name": it.name,
"description": it.description,
"category": it.category,
"icon_url": it.icon_url,
"is_active": it.is_active,
"trial_days": it.trial_days,
"trial_allowed": it.trial_allowed,
"created_at": it.created_at,
"updated_at": it.updated_at,
"plans": [
{
"id": p.id,
"period": p.period,
"price": float(p.price or 0),
"currency_id": p.currency_id,
"is_active": bool(p.is_active),
"created_at": p.created_at,
"updated_at": p.updated_at,
}
for p in plans
],
}
)
return result
# ========== Trial Functions ==========
def start_trial_plugin(
db: Session,
business_id: int,
plugin_id: int,
user_id: int,
) -> Dict[str, Any]:
"""
شروع دوره trial برای یک افزونه
هر کسب‌وکار فقط یکبار می‌تواند از trial استفاده کند
"""
# بررسی افزونه
plugin = db.query(MarketplacePlugin).filter(
MarketplacePlugin.id == plugin_id,
MarketplacePlugin.is_active == True # noqa: E712
).first()
if not plugin:
raise ApiError("PLUGIN_NOT_FOUND", "افزونه یافت نشد یا غیرفعال است", http_status=404)
# بررسی اینکه آیا trial مجاز است
if not plugin.trial_allowed or not plugin.trial_days or plugin.trial_days <= 0:
raise ApiError("TRIAL_NOT_ALLOWED", "این افزونه trial ندارد", http_status=400)
# بررسی کسب‌وکار
business = db.query(Business).filter(Business.id == int(business_id)).first()
if not business:
raise ApiError("BUSINESS_NOT_FOUND", "کسب‌وکار یافت نشد", http_status=404)
# بررسی اینکه آیا قبلاً trial استفاده شده است
existing_trial = (
db.query(BusinessPlugin)
.filter(
BusinessPlugin.business_id == int(business_id),
BusinessPlugin.plugin_id == plugin.id,
BusinessPlugin.is_trial == True # noqa: E712
)
.first()
)
if existing_trial:
raise ApiError("TRIAL_ALREADY_USED", "شما قبلاً از trial این افزونه استفاده کرده‌اید", http_status=400)
# بررسی اینکه آیا لایسنس فعال (غیر trial) وجود دارد
existing_license = (
db.query(BusinessPlugin)
.filter(
BusinessPlugin.business_id == int(business_id),
BusinessPlugin.plugin_id == plugin.id,
BusinessPlugin.is_trial == False # noqa: E712
)
.first()
)
if existing_license and existing_license.status == "active":
raise ApiError("PLUGIN_ALREADY_ACTIVE", "این افزونه قبلاً برای شما فعال شده است", http_status=400)
# پیدا کردن اولین پلن فعال (برای ثبت در BusinessPlugin)
plan = (
db.query(MarketplacePluginPlan)
.filter(
MarketplacePluginPlan.plugin_id == plugin.id,
MarketplacePluginPlan.is_active == True # noqa: E712
)
.order_by(MarketplacePluginPlan.price.asc())
.first()
)
if not plan:
raise ApiError("NO_PLAN_AVAILABLE", "هیچ پلن فعالی برای این افزونه وجود ندارد", http_status=400)
# محاسبه تاریخ شروع و پایان trial
starts_at = datetime.utcnow()
ends_at = starts_at + timedelta(days=plugin.trial_days)
# ایجاد لایسنس trial
trial_license = BusinessPlugin(
business_id=int(business_id),
plugin_id=plugin.id,
plan_id=plan.id, # استفاده از اولین پلن برای ثبت
status="active",
starts_at=starts_at,
ends_at=ends_at,
auto_renew=False,
is_trial=True,
trial_started_at=starts_at,
)
db.add(trial_license)
db.commit()
db.refresh(trial_license)
return {
"license_id": trial_license.id,
"plugin_id": plugin.id,
"plugin_code": plugin.code,
"plugin_name": plugin.name,
"plan_id": plan.id,
"status": "active",
"is_trial": True,
"is_active": True,
"starts_at": starts_at,
"ends_at": ends_at,
"trial_days": plugin.trial_days,
"trial_remaining_days": plugin.trial_days,
}
# ========== Business Plugin Status Functions ==========
def get_business_plugin_status(db: Session, business_id: int, plugin_id: int) -> Optional[Dict[str, Any]]:
now = datetime.utcnow()
license = (
db.query(BusinessPlugin)
.filter(
BusinessPlugin.business_id == int(business_id),
BusinessPlugin.plugin_id == int(plugin_id),
)
.first()
)
if not license:
return None
plugin = db.query(MarketplacePlugin).filter(MarketplacePlugin.id == plugin_id).first()
plan = db.query(MarketplacePluginPlan).filter(MarketplacePluginPlan.id == license.plan_id).first()
# بررسی انقضا
is_expired = False
if license.ends_at:
ends_at_val = license.ends_at.date() if isinstance(license.ends_at, datetime) else license.ends_at
now_val = now.date() if isinstance(now, datetime) else now
if ends_at_val < now_val:
is_expired = True
if license.status == "active":
license.status = "expired"
db.commit()
is_active = license.status == "active" and not is_expired
# محاسبه روزهای باقی‌مانده trial
trial_remaining_days = None
if license.is_trial and license.ends_at:
ends_at_val = license.ends_at.date() if isinstance(license.ends_at, datetime) else license.ends_at
now_val = now.date() if isinstance(now, datetime) else now
remaining = (ends_at_val - now_val).days
trial_remaining_days = max(0, remaining)
return {
"license_id": license.id,
"plugin_id": license.plugin_id,
"plugin_code": plugin.code if plugin else None,
"plugin_name": plugin.name if plugin else None,
"plan_id": license.plan_id,
"plan_period": plan.period if plan else None,
"status": license.status,
"is_active": is_active,
"is_expired": is_expired,
"is_trial": license.is_trial,
"trial_started_at": license.trial_started_at,
"trial_remaining_days": trial_remaining_days,
"starts_at": license.starts_at,
"ends_at": license.ends_at,
"auto_renew": license.auto_renew,
"created_at": license.created_at,
"updated_at": license.updated_at,
}
def list_business_plugins(db: Session, business_id: int) -> List[Dict[str, Any]]:
import logging
from sqlalchemy.exc import SQLAlchemyError
logger = logging.getLogger(__name__)
try:
now = datetime.utcnow()
licenses = (
db.query(BusinessPlugin)
.filter(BusinessPlugin.business_id == int(business_id))
.order_by(BusinessPlugin.id.desc())
.all()
)
result: List[Dict[str, Any]] = []
for license in licenses:
try:
plugin = db.query(MarketplacePlugin).filter(MarketplacePlugin.id == license.plugin_id).first()
plan = db.query(MarketplacePluginPlan).filter(MarketplacePluginPlan.id == license.plan_id).first()
# بررسی انقضا
is_expired = False
if license.ends_at:
ends_at_val = license.ends_at.date() if isinstance(license.ends_at, datetime) else license.ends_at
now_val = now.date() if isinstance(now, datetime) else now
if ends_at_val < now_val:
is_expired = True
if license.status == "active":
try:
license.status = "expired"
db.commit()
except SQLAlchemyError as e:
logger.warning(
f"Failed to update expired license status for license_id={license.id}: {str(e)}",
extra={"license_id": license.id, "business_id": business_id}
)
db.rollback()
# ادامه می‌دهیم حتی اگر commit fail شود
is_active = license.status == "active" and not is_expired
# محاسبه روزهای باقی‌مانده trial
trial_remaining_days = None
if license.is_trial and license.ends_at:
ends_at_val = license.ends_at.date() if isinstance(license.ends_at, datetime) else license.ends_at
now_val = now.date() if isinstance(now, datetime) else now
remaining = (ends_at_val - now_val).days
trial_remaining_days = max(0, remaining)
result.append({
"license_id": license.id,
"plugin_id": license.plugin_id,
"plugin_code": plugin.code if plugin else None,
"plugin_name": plugin.name if plugin else None,
"plan_id": license.plan_id,
"plan_period": plan.period if plan else None,
"plan_price": float(plan.price or 0) if plan else None,
"status": license.status,
"is_active": is_active,
"is_expired": is_expired,
"is_trial": license.is_trial,
"trial_started_at": license.trial_started_at,
"trial_remaining_days": trial_remaining_days,
"starts_at": license.starts_at,
"ends_at": license.ends_at,
"auto_renew": license.auto_renew,
"created_at": license.created_at,
"updated_at": license.updated_at,
})
except Exception as e:
# اگر خطا در پردازش یک license رخ دهد، آن را لاگ می‌کنیم و ادامه می‌دهیم
logger.warning(
f"Error processing license_id={license.id} for business_id={business_id}: {str(e)}",
exc_info=True,
extra={"license_id": license.id, "business_id": business_id}
)
# ادامه می‌دهیم تا سایر licenses را پردازش کنیم
return result
except SQLAlchemyError as e:
logger.error(
f"Database error in list_business_plugins for business_id={business_id}: {str(e)}",
exc_info=True,
extra={"business_id": business_id}
)
db.rollback()
raise
except Exception as e:
logger.error(
f"Unexpected error in list_business_plugins for business_id={business_id}: {str(e)}",
exc_info=True,
extra={"business_id": business_id}
)
raise
def check_and_update_expired_licenses(db: Session) -> Dict[str, Any]:
"""بررسی و به‌روزرسانی لایسنس‌های منقضی شده"""
now = datetime.utcnow()
expired_licenses = (
db.query(BusinessPlugin)
.filter(
BusinessPlugin.status == "active",
BusinessPlugin.ends_at.isnot(None),
BusinessPlugin.ends_at < now,
)
.all()
)
updated_count = 0
for license in expired_licenses:
license.status = "expired"
updated_count += 1
if updated_count > 0:
db.commit()
return {
"updated_count": updated_count,
"checked_at": now,
}
def sync_default_marketplace_plugins(db: Session) -> Dict[str, Any]:
"""ایجاد یا تکمیل افزونه‌های پیش‌فرض بازار (همسان با seed مهاجرت)."""
from adapters.db.seed_data.marketplace_plugins_seed import ensure_default_marketplace_plugins
result = ensure_default_marketplace_plugins(db)
if result.get("ok"):
db.commit()
return result