Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/hesabixAPI/adapters/api/v1/basalam_integration.py
2026-05-14 01:30:20 +03:30

482 lines
18 KiB
Python
Executable file

"""Basalam marketplace plugin APIs."""
from __future__ import annotations
from typing import Any, Dict, Optional
from fastapi import APIRouter, Body, Depends, Header, Path, Query, Request
from sqlalchemy.orm import Session
from adapters.db.session import get_db
from app.core.auth_dependency import AuthContext, get_current_user
from app.core.basalam_plugin_dependency import check_basalam_plugin_active
from app.core.i18n import locale_dependency
from app.core.permissions import require_business_access_dep, require_business_permission_dep
from app.core.responses import ApiError, success_response
from app.services import basalam_integration_service as basalam_svc
from app.services import basalam_reports_service as basalam_reports
from app.services.basalam_observability import get_basalam_metrics_summary
router = APIRouter(prefix="/basalam", tags=["یکپارچه‌سازی"])
def _ensure_plugin(db: Session, business_id: int) -> None:
if not check_basalam_plugin_active(db, business_id):
raise ApiError(
"BASALAM_PLUGIN_NOT_ACTIVE",
"Basalam plugin is not active.",
http_status=403,
details={"plugin_code": "basalam_connector"},
)
@router.get("/observability/metrics-summary")
def basalam_observability_metrics_summary(
request: Request,
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
) -> Dict[str, Any]:
"""شمارندهٔ تجمیعی عملیات باسلام (Redis). نیازمند دسترسی مدیر سیستم یا سوپرادمین."""
if not _ctx.has_any_permission("system_settings", "superadmin"):
raise ApiError(
"FORBIDDEN",
"برای مشاهدهٔ متریک‌های باسلام نیاز به دسترسی مدیر سیستم است.",
http_status=403,
)
return success_response(get_basalam_metrics_summary(), request)
@router.get("/business/{business_id}/currency-readiness")
def get_basalam_currency_readiness(
request: Request,
business_id: int = Path(..., gt=0),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "view")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.get_basalam_currency_readiness(db, business_id)
return success_response(data, request)
@router.get("/business/{business_id}/reports/overview")
def basalam_reports_overview(
request: Request,
business_id: int = Path(..., gt=0),
chart_days: int = Query(90, ge=7, le=366),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("reports", "view")),
____: None = Depends(require_business_permission_dep("basalam", "view")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_reports.get_overview(db, business_id, chart_days=chart_days)
return success_response(data, request)
@router.get("/business/{business_id}/reports/synced-invoices")
def basalam_reports_synced_invoices(
request: Request,
business_id: int = Path(..., gt=0),
date_from: Optional[str] = Query(None),
date_to: Optional[str] = Query(None),
skip: int = Query(0, ge=0),
take: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("reports", "view")),
____: None = Depends(require_business_permission_dep("basalam", "view")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
df = basalam_reports.parse_report_date(date_from)
dt = basalam_reports.parse_report_date(date_to)
data = basalam_reports.list_synced_invoices(db, business_id, date_from=df, date_to=dt, skip=skip, take=take)
return success_response(data, request)
@router.get("/business/{business_id}/reports/dead-letter")
def basalam_reports_dead_letter(
request: Request,
business_id: int = Path(..., gt=0),
item_type: Optional[str] = Query(None),
limit: int = Query(100, ge=1, le=200),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("reports", "view")),
____: None = Depends(require_business_permission_dep("basalam", "view")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_reports.list_dead_letter_for_report(
db, business_id, item_type=item_type, limit=limit, offset=offset
)
return success_response(data, request)
@router.get("/business/{business_id}/reports/product-conflicts")
def basalam_reports_product_conflicts(
request: Request,
business_id: int = Path(..., gt=0),
conflict_type: Optional[str] = Query(None),
direction: Optional[str] = Query(None),
search: Optional[str] = Query(None),
sort_by: str = Query("created_at"),
sort_dir: str = Query("desc"),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("reports", "view")),
____: None = Depends(require_business_permission_dep("basalam", "view")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_reports.list_product_conflicts_for_report(
db,
business_id,
conflict_type=conflict_type,
direction=direction,
search=search,
sort_by=sort_by,
sort_dir=sort_dir,
limit=limit,
offset=offset,
)
return success_response(data, request)
@router.get("/business/{business_id}/settings")
def get_basalam_settings(
request: Request,
business_id: int = Path(..., gt=0),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "view")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.get_settings(db, business_id)
return success_response(data, request)
@router.put("/business/{business_id}/settings")
def put_basalam_settings(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "manage")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.update_settings(db, business_id, payload or {})
return success_response(data, request)
@router.post("/business/{business_id}/sync/orders")
def manual_sync_basalam_orders(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.manual_sync_orders(db, business_id, payload or {}, user_id=_ctx.get_user_id())
return success_response(data, request)
@router.post("/business/{business_id}/sync/products")
def manual_sync_basalam_products(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.manual_sync_products(db, business_id, payload or {}, user_id=_ctx.get_user_id())
return success_response(data, request)
@router.post("/business/{business_id}/sync/products/publish")
def publish_basalam_products(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.publish_products_to_basalam(db, business_id, payload or {}, user_id=_ctx.get_user_id())
return success_response(data, request)
@router.post("/business/{business_id}/sync/products/pull")
def pull_basalam_products(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(default={}),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.pull_products_from_basalam(db, business_id, payload or {}, user_id=_ctx.get_user_id())
return success_response(data, request)
@router.post("/business/{business_id}/sync/products/push/incremental")
def push_basalam_products_incremental(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(default={}),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.push_products_incremental(db, business_id, payload or {}, user_id=_ctx.get_user_id())
return success_response(data, request)
@router.post("/business/{business_id}/sync/products/publish/retry")
def retry_basalam_products_publish(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(default={}),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.retry_failed_product_publishes(db, business_id, payload or {}, user_id=_ctx.get_user_id())
return success_response(data, request)
@router.get("/business/{business_id}/sync/products/conflicts")
def list_basalam_product_conflicts(
request: Request,
business_id: int = Path(..., gt=0),
conflict_type: Optional[str] = Query(None),
direction: Optional[str] = Query(None),
search: Optional[str] = Query(None),
sort_by: Optional[str] = Query("created_at"),
sort_dir: Optional[str] = Query("desc"),
limit: int = Query(25, ge=1, le=200),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.list_product_conflicts(
db,
business_id,
conflict_type=conflict_type,
direction=direction,
search=search,
sort_by=sort_by,
sort_dir=sort_dir,
limit=limit,
offset=offset,
)
return success_response(data, request)
@router.delete("/business/{business_id}/sync/products/conflicts")
def clear_basalam_product_conflicts(
request: Request,
business_id: int = Path(..., gt=0),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.clear_product_conflicts(db, business_id)
return success_response(data, request)
@router.post("/business/{business_id}/sync/products/conflicts/resolve")
def resolve_basalam_product_conflicts(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.resolve_product_conflicts(db, business_id, payload or {}, user_id=_ctx.get_user_id())
return success_response(data, request)
@router.post("/business/{business_id}/sync/payments/unverified")
def sync_basalam_unverified_payments(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(default={}),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
verify_remote = payload.get("verify_remote")
data = basalam_svc.sync_unverified_payments(
db=db,
business_id=business_id,
user_id=_ctx.get_user_id(),
verify_remote=bool(verify_remote) if verify_remote is not None else None,
)
return success_response(data, request)
@router.get("/business/{business_id}/sync/dead-letter")
def list_basalam_sync_dead_letter(
request: Request,
business_id: int = Path(..., gt=0),
item_type: Optional[str] = Query(None),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.list_sync_dead_letter(
db,
business_id,
limit=limit,
offset=offset,
item_type=item_type,
)
return success_response(data, request)
@router.post("/business/{business_id}/sync/dead-letter/clear")
def clear_basalam_sync_dead_letter(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(default_factory=dict),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "manage")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
body = payload or {}
if body.get("all") is True:
data = basalam_svc.clear_sync_dead_letter(db, business_id, mode="all")
elif isinstance(body.get("dlq_ids"), list):
data = basalam_svc.clear_sync_dead_letter(db, business_id, mode="ids", dlq_ids=body.get("dlq_ids"))
else:
raise ApiError(
"BASALAM_DLQ_CLEAR_PAYLOAD",
'Body must include "all": true or "dlq_ids": [...].',
http_status=400,
)
return success_response(data, request)
@router.post("/business/{business_id}/sync/chats/inbound")
async def sync_basalam_inbound_chats(
request: Request,
business_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "sync")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = basalam_svc.sync_inbound_chat_messages(
db=db,
business_id=business_id,
payload=payload or {},
user_id=_ctx.get_user_id(),
)
return success_response(data, request)
@router.post("/business/{business_id}/chats/{conversation_id}/reply")
async def send_basalam_chat_reply(
request: Request,
business_id: int = Path(..., gt=0),
conversation_id: int = Path(..., gt=0),
payload: Dict[str, Any] = Body(...),
db: Session = Depends(get_db),
_ctx: AuthContext = Depends(get_current_user),
_: None = Depends(locale_dependency),
__: None = Depends(require_business_access_dep),
___: None = Depends(require_business_permission_dep("basalam", "manage")),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
data = await basalam_svc.send_chat_reply_to_basalam(
db=db,
business_id=business_id,
conversation_id=conversation_id,
body=str(payload.get("body") or payload.get("text") or ""),
user_id=_ctx.get_user_id(),
basalam_chat_id=payload.get("chat_id"),
)
return success_response(data, request)
@router.post("/webhook/{business_id}")
async def basalam_webhook(
request: Request,
business_id: int = Path(..., gt=0),
x_basalam_signature: Optional[str] = Header(None),
db: Session = Depends(get_db),
) -> Dict[str, Any]:
_ensure_plugin(db, business_id)
payload = await request.json()
raw_body = await request.body()
data = basalam_svc.process_webhook(
db=db,
business_id=business_id,
payload=payload if isinstance(payload, dict) else {},
raw_body=raw_body,
signature=x_basalam_signature,
user_id=None,
)
return success_response(data, request)