From b0fa541fbc96b7a43f73fe674e4caefe5d34a170 Mon Sep 17 00:00:00 2001 From: adriano Date: Fri, 13 Jun 2025 18:14:16 -0300 Subject: [PATCH] added card total cost --- backend/Pipfile | 2 + backend/Pipfile.lock | 26 ++- backend/app/__init__.py | 3 +- backend/app/config.py | 2 +- backend/app/extensions.py | 6 + backend/app/routes/usage_routes.py | 70 +++--- backend/app/services/redis_service.py | 13 ++ backend/app/services/report_service.py | 51 ++-- frontend/app/dashboard/page.tsx | 2 +- frontend/components/transcription-table.tsx | 244 +++++++++++++++++--- 10 files changed, 330 insertions(+), 89 deletions(-) create mode 100644 backend/app/services/redis_service.py diff --git a/backend/Pipfile b/backend/Pipfile index 0fc95a2..0a9271e 100644 --- a/backend/Pipfile +++ b/backend/Pipfile @@ -20,6 +20,8 @@ flask-bcrypt = "*" flask-cors = "*" mypy = "*" requests = "*" +redis = "*" +flask-redis = "*" [dev-packages] diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 4f284a6..178f214 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "3798a8dec1f82bd3b8d8e6089e8110ae5e152e1e76d9b78810ba5b4cfab33c2f" + "sha256": "424e88c981f7de9a38bc3d2c0189df4f377bb72daa06d4b7a0b742a51b7ad51e" }, "pipfile-spec": 6, "requires": { @@ -276,6 +276,15 @@ "markers": "python_version >= '3.9' and python_version < '4'", "version": "==4.7.1" }, + "flask-redis": { + "hashes": [ + "sha256:8d79eef4eb1217095edab603acc52f935b983ae4b7655ee7c82c0dfd87315d17", + "sha256:e1fccc11e7ea35c2a4d68c0b9aa58226a098e45e834d615c7b6c4928b01ddd6c" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==0.4.0" + }, "flask-restx": { "hashes": [ "sha256:4f3d3fa7b6191fcc715b18c201a12cd875176f92ba4acc61626ccfd571ee1728", @@ -664,12 +673,12 @@ }, "pydantic": { "hashes": [ - "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", - "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7" + "sha256:12b45cfb4af17e555d3c6283d0b55271865fb0b43cc16dd0d52749dc7abf70e7", + "sha256:a24478d2be1b91b6d3bc9597439f69ed5e87f68ebd285d86f7c7932a084b72e7" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==2.11.5" + "version": "==2.11.6" }, "pydantic-core": { "hashes": [ @@ -906,6 +915,15 @@ ], "version": "==2025.2" }, + "redis": { + "hashes": [ + "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", + "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==6.2.0" + }, "referencing": { "hashes": [ "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", diff --git a/backend/app/__init__.py b/backend/app/__init__.py index cc31707..d71070f 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -1,7 +1,7 @@ from flask import Flask from flask_restx import Api from .config import Config -from .extensions import init_mongo, init_jwt +from .extensions import init_mongo, init_jwt, init_redis from .errors.handlers import register_error_handlers from .routes.usage_routes import usage_ns from .routes.auth_routes import auth_ns @@ -16,6 +16,7 @@ def create_app(): init_mongo(app) init_jwt(app) + init_redis(app) api = Api( app, diff --git a/backend/app/config.py b/backend/app/config.py index 9e24aab..7a6e204 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -26,6 +26,6 @@ class Config: BILLING_API_URL = os.getenv("BILLING_API_URL") BILLING_API_TOKEN = os.getenv("BILLING_API_TOKEN") - + REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379") diff --git a/backend/app/extensions.py b/backend/app/extensions.py index 3e92de6..d0b5741 100644 --- a/backend/app/extensions.py +++ b/backend/app/extensions.py @@ -1,10 +1,16 @@ from pymongo import MongoClient from flask_jwt_extended import JWTManager +from flask_redis import FlaskRedis jwt = JWTManager() +redis_client = FlaskRedis() + def init_mongo(app): app.mongo_client = MongoClient(app.config["MONGO_URI"]) +def init_redis(app): + redis_client.init_app(app) + def init_jwt(app): jwt.init_app(app) \ No newline at end of file diff --git a/backend/app/routes/usage_routes.py b/backend/app/routes/usage_routes.py index cbbf262..8a6ad90 100644 --- a/backend/app/routes/usage_routes.py +++ b/backend/app/routes/usage_routes.py @@ -1,4 +1,5 @@ -import os +import os +import hashlib from bson import json_util from flask_restx import Resource from flask_jwt_extended import jwt_required @@ -16,18 +17,20 @@ from app.docs.usage_models import ( transcription_data_query_params, usage_ns) +TMP_DIR = '/tmp' + @usage_ns.route('/export/trascription') class TranscriptionExport(Resource): @usage_ns.doc(security='Bearer Auth') @usage_ns.doc(params=transcription_data_query_params) @usage_ns.response(200, 'success') - @usage_ns.response(400, 'Validation error') + @usage_ns.response(400, 'Validation error') @usage_ns.response(404, 'File not found') @jwt_required() def get(self): """ Export transcription report in XLSX. - """ + """ data = { "company_id": request.args.get("companyId", type=str), "start_date": request.args.get("startDate"), @@ -37,35 +40,39 @@ class TranscriptionExport(Resource): validated = TranscriptionRequest(**data) - service = TranscriptionReportService( - validated.company_id, - validated.start_date, - validated.end_date + # Cria um nome de arquivo baseado nos parâmetros (evita caracteres inválidos) + hash_key = hashlib.md5(f"{validated.company_id}_{validated.start_date}_{validated.end_date}_{validated.who}".encode()).hexdigest() + filename = f"transcription_{hash_key}.xlsx" + filepath = os.path.join(TMP_DIR, filename) + + # Verifica se o arquivo já existe + if not os.path.exists(filepath): + # Gera o relatório e salva no caminho desejado + service = TranscriptionReportService( + validated.company_id, + validated.start_date, + validated.end_date + ) + + if validated.who == "hit": + generated_path = service.reportDataXLSX(hit_report=True) + else: + generated_path = service.reportDataXLSX() + + if not os.path.exists(generated_path): + return {"error": "File generation failed"}, 500 + + # Move o arquivo gerado para o TMP + os.rename(generated_path, filepath) + + # Retorna o arquivo + return send_file( + path_or_file=filepath, + as_attachment=True, + download_name=os.path.basename(filepath), + mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) - if validated.who == "hit": - report_path = service.reportDataXLSX(hit_report=True) - else: - report_path = service.reportDataXLSX() - - if not os.path.exists(report_path): - return {"error": "File not found"}, 404 - - @after_this_request - def remove_file(response): - try: - os.remove(report_path) - except Exception as delete_error: - current_app.logger.warning(f"Error trying to delete file: {delete_error}") - return response - - return send_file( - path_or_file=report_path, - as_attachment=True, - download_name=os.path.basename(report_path), - mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' - ) - @usage_ns.route('/data/trascription') class TranscriptionUsageData(Resource): @@ -95,13 +102,14 @@ class TranscriptionUsageData(Resource): validated.start_date, validated.end_date ) + if validated.who == "hit": result = service.reportData(page=page, page_size=page_size,hit_report=True) else: result = service.reportData(page=page, page_size=page_size) - return {"success": True, "data": {"data": result["data"], "pagination": result["pagination"]}}, 200 + return {"success": True, "data": {"data": result["data"], "pagination": result["pagination"], "cost":result["cost"]}}, 200 diff --git a/backend/app/services/redis_service.py b/backend/app/services/redis_service.py new file mode 100644 index 0000000..1960829 --- /dev/null +++ b/backend/app/services/redis_service.py @@ -0,0 +1,13 @@ +from app.extensions import redis_client + +def cache_set(key, value, ex=0): + if ex: + redis_client.set(key, value, ex=ex) + else: + redis_client.set(key, value) + +def cache_get(key): + return redis_client.get(key) + +def cache_del(key): + redis_client.delete(key) \ No newline at end of file diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index 2d584a0..56f16b6 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -7,11 +7,12 @@ from openpyxl.utils.dataframe import dataframe_to_rows from openpyxl.styles import Font, PatternFill import pandas as pd import os +import hashlib from typing import List, Dict, Any, Optional from app.utils.mysql_query import execute_query from app.utils.calc_api_usage import calculate_api_usage -import math - +from app.services.redis_service import cache_del, cache_get, cache_set +import json class TranscriptionReportService: def __init__(self, company_id: str, start_date: str, end_date: str): @@ -155,10 +156,7 @@ class TranscriptionReportService: "page_size": page_size, "total_pages": (total + page_size - 1) // page_size } - - - - + def _fetch_mysql_data(self, hit_report: Optional[bool] = False)-> List[Dict[str, Any]]: @@ -185,12 +183,7 @@ class TranscriptionReportService: uniqueid, src, dst;""" rows = execute_query(self.company_id, sql) - if hit_report: - # collection = self.mongo_client["billing-api"]["api_pricings"] - # result_stt = collection.find({"type": "stt"}) - - # result_stt = [{"product": t["product"], "clientPrice": t["clientPrice"]} for t in result_stt if "clientPrice" in t] - + if hit_report: for row in rows: row["companyId"] = self.company_id @@ -198,6 +191,7 @@ class TranscriptionReportService: row["custo_hit"] = f"{float(rowMongo["totalCost"])}" row["qtd_token_input"] = rowMongo.get('usageByType', {}).get('input', 0) row["qtd_token_output"] = rowMongo.get('usageByType', {}).get('output', 0) + row["total_min"] = f"{(int(row['total_billsec']) / 60):.2f}" self.client_price_row(products, row) self.formate_properties(row) @@ -209,6 +203,8 @@ class TranscriptionReportService: self.client_price_row(products, row) self.formate_properties(row) + + return rows def formate_properties(self, row): @@ -258,6 +254,7 @@ class TranscriptionReportService: "src": "Origem", "dst": "Destino", "total_billsec": "Quantidade de segundos", + "total_min": "Duração (Em minutos)", "custo_hit": "Custo HIT", "qtd_token_input": "Quantidade de tokens(input)", "qtd_token_output": "Quantidade de tokens(output)", @@ -311,7 +308,34 @@ class TranscriptionReportService: wb.save(path) return path + + def _reportDataTotalCost(self): + + hash_key = hashlib.md5(f"{self.company_id}_{self.start_date}_{self.end_date}".encode()).hexdigest() + sum_key = f"sum_total_cost_{hash_key}" + + if data := cache_get(f'report_model_usage:total_cost:{sum_key}'): + return json.loads(data) + self._fetch_mongo_data(all_data=True) + mysql_data = self._fetch_mysql_data(hit_report=True) + + total_cost_hit = sum(float(item.get('custo_hit') or 0) for item in mysql_data) + total_client_cost = sum(float(item.get('client_total_cost') or 0) for item in mysql_data) + + sum_total_cost = { + 'company_id': self.company_id, + 'start_date': self.start_date, + 'end_date': self.end_date, + 'total_cost_hit': total_cost_hit, + 'total_client_cost': total_client_cost + } + + cache_set(f'report_model_usage:total_cost:{sum_key}', json.dumps(sum_total_cost)) + + return sum_total_cost + + def reportDataXLSX(self, hit_report: Optional[bool] = False) -> str: self._fetch_mongo_data(all_data=True) @@ -333,7 +357,8 @@ class TranscriptionReportService: return { "pagination": mongo_data, - "data": mysql_data + "data": mysql_data, + "cost": self._reportDataTotalCost() } diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx index 11dc478..139cd8a 100644 --- a/frontend/app/dashboard/page.tsx +++ b/frontend/app/dashboard/page.tsx @@ -48,7 +48,7 @@ export default function Dashboard() {
-

Dashboard - Sistema de Transcrição

+

Dashboard - Custo de Produtos