from sqlalchemy.orm import Session from app.modules.audit.constants import AuditAction, AuditSource from app.modules.audit.schemas import AuditLogCreate from app.modules.audit.service import AuditService from app.modules.events.constants import ( EventAggregateType, EventPayloadKey, EventSource, EventType, ) from app.modules.events.services import EventService from app.modules.feishu.service import FeishuService from app.modules.reports.constants import ( ReportPushStatus, ReportResponseKey, ) from app.modules.reports.chart import lifecycle_chart_alt, render_lifecycle_chart from app.modules.reports.services import ReportService class ReportDeliveryService: """Deliver generated reports without coupling report generation to Feishu.""" def __init__(self, db: Session): self.db = db self.reports = ReportService(db) def push_report( self, report: dict, receive_id: str | None, receive_id_type: str, actor: str, push_run_code: str | None = None, ) -> dict: report_type = str(report.get(ReportResponseKey.REPORT_TYPE) or report.get("type") or "report") title = report.get(ReportResponseKey.TITLE) push_run = ( self.reports._get_push_run(push_run_code) if push_run_code else self.reports.create_push_run( report_type=report_type, title=title, receive_id=receive_id, receive_id_type=receive_id_type, actor=actor, ) ) try: feishu = FeishuService(self.db) image_key = None image_alt = None chart_data = report.get("chart_data") if chart_data: image_result = feishu.upload_image(render_lifecycle_chart(chart_data), actor) image_key = (image_result.get("data") or {}).get("image_key") if not image_key: raise ValueError("Feishu image upload did not return image_key") image_alt = lifecycle_chart_alt(chart_data) card = FeishuService.build_basic_card( report[ReportResponseKey.TITLE], report[ReportResponseKey.LINES], image_key=image_key, image_alt=image_alt, ) result = feishu.send_card(card, receive_id, receive_id_type, actor) except Exception as exc: failed_run = self.reports.update_push_run( push_run.code, ReportPushStatus.FAILED, error_message=str(exc), commit=False, ) EventService(self.db).enqueue( event_type=EventType.REPORT_PUSH_FAILED, source=EventSource.REPORTS, aggregate_type=EventAggregateType.REPORT_PUSH_RUN, aggregate_id=failed_run.code, actor=actor, payload={ EventPayloadKey.CODE: failed_run.code, EventPayloadKey.STATUS: failed_run.status, EventPayloadKey.ERROR_MESSAGE: failed_run.error_message, }, idempotency_key=f"report-push:{failed_run.code}:{failed_run.status}", ) self.db.commit() raise success_run = self.reports.update_push_run( push_run.code, ReportPushStatus.SUCCESS, provider_response=result, sent=True, commit=False, ) EventService(self.db).enqueue( event_type=EventType.REPORT_PUSH_SUCCEEDED, source=EventSource.REPORTS, aggregate_type=EventAggregateType.REPORT_PUSH_RUN, aggregate_id=success_run.code, actor=actor, payload={ EventPayloadKey.CODE: success_run.code, EventPayloadKey.STATUS: success_run.status, }, idempotency_key=f"report-push:{success_run.code}:{success_run.status}", ) AuditService(self.db).record( AuditLogCreate( actor=actor, source=AuditSource.REPORTS, action=AuditAction.REPORT_PUSH, target_id=push_run.code, response_payload={"status": ReportPushStatus.SUCCESS}, ) ) self.db.commit() return result