From 0329997141a9da082669751d1eb9e0a0c892f09f Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Thu, 27 Mar 2025 22:33:11 -0300 Subject: [PATCH 01/23] Chore: Add NotificationPayload TypedDict to schema.py for MercadoPago integration --- project/ext/payment_gateways/mercadopago/schema.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/project/ext/payment_gateways/mercadopago/schema.py b/project/ext/payment_gateways/mercadopago/schema.py index ce6642b..3bbff0a 100644 --- a/project/ext/payment_gateways/mercadopago/schema.py +++ b/project/ext/payment_gateways/mercadopago/schema.py @@ -6,6 +6,17 @@ class Payer(TypedDict): first_name: str +class NotificationPayload(TypedDict): + action: str + api_version: str + data: dict[str, int] + date_created: str + id: str + live_mode: bool + type: str + user_id: int + + class PaymentPayload(TypedDict): payer: Payer installments: int From 546d828aa97b48203709f12d040f702d1e98ba34 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 10:05:34 -0300 Subject: [PATCH 02/23] Chore: Update database and Redis URLs in settings.toml for containerized environment --- settings.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.toml b/settings.toml index 693784d..9b8a44c 100644 --- a/settings.toml +++ b/settings.toml @@ -19,8 +19,8 @@ FLASK_ENV = "local" INCLUDES = ["default"] TEMPLATES_AUTO_RELOAD = true DEBUG = true -SQLALCHEMY_DATABASE_URI = "postgresql://myuser:mypassword1@localhost:5432/mydatabase" -REDIS_URL = "redis://localhost:6379" +SQLALCHEMY_DATABASE_URI = "postgresql://myuser:mypassword1@postgres:5432/mydatabase" +REDIS_URL = "redis://redis:6379" NOTIFICATION_URL = '' [staging] From 609afc6c88e30e951760d35b0bff8bdcbf84033c Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 10:40:41 -0300 Subject: [PATCH 03/23] feat: Add notification endpoint to REST API --- project/ext/restapi/__init__.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/project/ext/restapi/__init__.py b/project/ext/restapi/__init__.py index 07e0250..1c9fb28 100644 --- a/project/ext/restapi/__init__.py +++ b/project/ext/restapi/__init__.py @@ -1,31 +1,36 @@ +from flask_restx import Resource + from project.controller.lead_controller import LeadResource from project.controller.user_controller import UserResource, UserResourceID from project.doc_model.doc_models import ( api, bp, client_model, + establishment_model, lead_model, order_model, payment_model, product_model, - establishment_model, user_model, ) from project.utils.namespace import ( client_ns, + establishment_ns, lead_ns, order_ns, payment_ns, product_ns, - establishment_ns, user_ns, ) from ...controller.client_controller import ClientResource, ClientResourceID +from ...controller.establishment_controller import ( + EstablishmentResource, + EstablishmentResourceID, +) from ...controller.order_controller import OrderResource, OrderResourceID from ...controller.payment_controller import PaymentResource, PaymentResourceID from ...controller.product_controller import ProductResource, ProductResourceID -from ...controller.establishment_controller import EstablishmentResource, EstablishmentResourceID establishment_ns.models["EstablishmentModel"] = establishment_model user_ns.models["UserModel"] = user_model @@ -64,6 +69,12 @@ api.add_namespace(lead_ns) +@api.route("/notify") +class Notification(Resource): + def post(self): + return {"content": api.payload} + + def init_app(app): app.register_blueprint(bp) api.add_namespace(establishment_ns) From 339d4033fadbc58a49305db6b3e8fee164bf0f61 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 10:47:50 -0300 Subject: [PATCH 04/23] feat: Change command to run Flask application in Dockerfile for test purposes --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cd12970..7b212bf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,5 +19,6 @@ ENV PATH=/root/.local/bin:$PATH EXPOSE 5000 -CMD ["gunicorn","-w", "4", "-b", "0.0.0.0:5000", "project:create_app()"] +# CMD ["gunicorn","-w", "4", "-b", "0.0.0.0:5000", "project:create_app()"] +CMD ["flask","run", "--host", "0.0.0.0", "--port", "5000"] From c54c0b0237115e03e1f0c492f86399e37f45a237 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 10:47:57 -0300 Subject: [PATCH 05/23] feat: Log payload in notification endpoint for debugging purposes --- project/ext/restapi/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/project/ext/restapi/__init__.py b/project/ext/restapi/__init__.py index 1c9fb28..232ea92 100644 --- a/project/ext/restapi/__init__.py +++ b/project/ext/restapi/__init__.py @@ -1,3 +1,4 @@ +from click import echo from flask_restx import Resource from project.controller.lead_controller import LeadResource @@ -72,6 +73,7 @@ @api.route("/notify") class Notification(Resource): def post(self): + echo(api.payload) return {"content": api.payload} From 554b2b50eded67136b755fbe7abc20fbaa27f063 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 10:53:47 -0300 Subject: [PATCH 06/23] feat: Log request headers in notification endpoint for improved debugging --- project/ext/restapi/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/project/ext/restapi/__init__.py b/project/ext/restapi/__init__.py index 232ea92..cc88c5b 100644 --- a/project/ext/restapi/__init__.py +++ b/project/ext/restapi/__init__.py @@ -1,4 +1,5 @@ from click import echo +from flask import request from flask_restx import Resource from project.controller.lead_controller import LeadResource @@ -74,6 +75,8 @@ class Notification(Resource): def post(self): echo(api.payload) + headers = request.headers + echo(f"Request Headers: {headers}") return {"content": api.payload} From 8e035f4e36954a6390823edd1d4fa96c9c2424a6 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 12:28:24 -0300 Subject: [PATCH 07/23] feat: Update database and Redis connection strings in secrets configuration --- .secrets.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.secrets.toml b/.secrets.toml index f1f1546..ca34fb5 100644 --- a/.secrets.toml +++ b/.secrets.toml @@ -5,8 +5,8 @@ MERCADOPAGO_ACCESS_TOKEN = "TEST-3070206317287648-031707-c6e11b557ecde6fb4740943 [staging] -SQLALCHEMY_DATABASE_URI = "postgresql://postgres.pnetnjaftmfzgumhgvit:Hampersoujr24@aws-0-sa-east-1.pooler.supabase.com:5432/postgres" -REDIS_URL = "rediss://red-cus5n0an91rc73di3440:NCMGqJ6UjrX2Xmf1FwQTFU2kmv75q7Xa@oregon-keyvalue.render.com:6379" +SQLALCHEMY_DATABASE_URI = "postgresql://myuser:mypassword1@postgres:5432/mydatabase" +REDIS_URL = "redis://redis:6379" [production] From 5f1e8d51e514a15534cbc8ae24049473e582b1e2 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 12:28:38 -0300 Subject: [PATCH 08/23] feat: Restore gunicorn command in Dockerfile for production deployment --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7b212bf..b8b5598 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,6 @@ ENV PATH=/root/.local/bin:$PATH EXPOSE 5000 -# CMD ["gunicorn","-w", "4", "-b", "0.0.0.0:5000", "project:create_app()"] -CMD ["flask","run", "--host", "0.0.0.0", "--port", "5000"] +CMD ["gunicorn","-w", "4", "-b", "0.0.0.0:5000", "project:create_app()"] +# CMD ["flask","run", "--host", "0.0.0.0", "--port", "5000"] From 3f43d46b2511e5f2cdfdf98debc10877bb1c6893 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 12:28:55 -0300 Subject: [PATCH 09/23] feat: Refactor namespace definitions for improved readability and consistency --- project/utils/namespace.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/project/utils/namespace.py b/project/utils/namespace.py index 315f3a3..a2e3bf6 100644 --- a/project/utils/namespace.py +++ b/project/utils/namespace.py @@ -1,23 +1,20 @@ from flask_restx import Namespace establishment_ns = Namespace( - name="Establishment", description="Gerenciar estabelecimento", path="/establishments" -) -user_ns = Namespace( - name="User", description="Gerenciar usuário", path="/users" + name="Establishment", + description="Gerenciar estabelecimento", + path="/establishments", ) +user_ns = Namespace(name="User", description="Gerenciar usuário", path="/users") product_ns = Namespace( name="Product", description="Gerenciar produto", path="/products" ) -client_ns = Namespace( - name="Client", description="Gerenciar cliente", path="/clients" -) -order_ns = Namespace( - name="Order", description="Gerenciar pedido", path="/orders" -) +client_ns = Namespace(name="Client", description="Gerenciar cliente", path="/clients") +order_ns = Namespace(name="Order", description="Gerenciar pedido", path="/orders") payment_ns = Namespace( name="Payment", description="Gerenciar pagamento", path="/payments" ) -lead_ns = Namespace( - name="Lead", description="Gerenciar lead", path="/leads" +lead_ns = Namespace(name="Lead", description="Gerenciar lead", path="/leads") +notification_ns = Namespace( + name="Notification", description="Gerenciar Notificações", path="/notify" ) From b9ff26dd7a1254cf3da01de9e7e484968b0552bb Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 12:29:38 -0300 Subject: [PATCH 10/23] feat: Implement notification endpoint for handling incoming notifications --- project/base.py | 15 +-------------- project/controller/notification_controller.py | 18 ++++++++++++++++++ project/ext/restapi/__init__.py | 18 +++++------------- 3 files changed, 24 insertions(+), 27 deletions(-) create mode 100644 project/controller/notification_controller.py diff --git a/project/base.py b/project/base.py index 9c9f6aa..aba0e06 100644 --- a/project/base.py +++ b/project/base.py @@ -3,7 +3,7 @@ """ from dynaconf import FlaskDynaconf -from flask import Flask, request +from flask import Flask from flask_cors import CORS @@ -18,7 +18,6 @@ def create_app(**config): app.config.load_extensions("EXTENSIONS") # type: ignore app.config.update(config) - # Configuração do CORS para permitir requisições do frontend CORS( app, resources={ @@ -35,13 +34,7 @@ def create_app(**config): }, supports_credentials=True, ) - # 🔹 Intercepta e responde a requisições OPTIONS antes que o navegador bloqueie - @app.before_request - def handle_preflight(): - if request.method == "OPTIONS": - return "", 200 - print(f"Ambiente atual: {app.config.env}") # type: ignore print(f"Banco de dados atual: {app.config.get('SQLALCHEMY_DATABASE_URI')}") print("Aplicação inicializada com sucesso!") @@ -54,9 +47,3 @@ def create_app_wsgi(): Método que inicializa o app """ return create_app() - - -# 🔹 Permite rodar o servidor diretamente -if __name__ == "__main__": - app = create_app() - app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/project/controller/notification_controller.py b/project/controller/notification_controller.py new file mode 100644 index 0000000..bec8c6a --- /dev/null +++ b/project/controller/notification_controller.py @@ -0,0 +1,18 @@ +from click import echo +from flask import request +from flask_restx import Resource + + +class NotificationResource(Resource): + def post(self): + try: + notification_data = request.json + if not isinstance(notification_data, dict): + return {"error": "Invalid input"}, 400 + + echo(notification_data) + + return {}, 200 + + except Exception as e: + return {"error": str(e)}, 400 diff --git a/project/ext/restapi/__init__.py b/project/ext/restapi/__init__.py index cc88c5b..c0ff970 100644 --- a/project/ext/restapi/__init__.py +++ b/project/ext/restapi/__init__.py @@ -1,7 +1,3 @@ -from click import echo -from flask import request -from flask_restx import Resource - from project.controller.lead_controller import LeadResource from project.controller.user_controller import UserResource, UserResourceID from project.doc_model.doc_models import ( @@ -19,6 +15,7 @@ client_ns, establishment_ns, lead_ns, + notification_ns, order_ns, payment_ns, product_ns, @@ -30,6 +27,7 @@ EstablishmentResource, EstablishmentResourceID, ) +from ...controller.notification_controller import NotificationResource from ...controller.order_controller import OrderResource, OrderResourceID from ...controller.payment_controller import PaymentResource, PaymentResourceID from ...controller.product_controller import ProductResource, ProductResourceID @@ -61,6 +59,7 @@ payment_ns.add_resource(PaymentResourceID, "/") lead_ns.add_resource(LeadResource, "/") +notification_ns.add_resource(NotificationResource) api.add_namespace(establishment_ns) api.add_namespace(user_ns) @@ -69,15 +68,7 @@ api.add_namespace(order_ns) api.add_namespace(payment_ns) api.add_namespace(lead_ns) - - -@api.route("/notify") -class Notification(Resource): - def post(self): - echo(api.payload) - headers = request.headers - echo(f"Request Headers: {headers}") - return {"content": api.payload} +api.add_namespace(notification_ns) def init_app(app): @@ -89,3 +80,4 @@ def init_app(app): api.add_namespace(order_ns) api.add_namespace(payment_ns) api.add_namespace(lead_ns) + api.add_namespace(notification_ns) From 1d9f9cbe3a74e28bb7751efa5344b64d3639d9b0 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 12:44:42 -0300 Subject: [PATCH 11/23] feat: Add Docker Compose configuration for backend service --- docker-compose.ext.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 docker-compose.ext.yml diff --git a/docker-compose.ext.yml b/docker-compose.ext.yml new file mode 100644 index 0000000..4b615a9 --- /dev/null +++ b/docker-compose.ext.yml @@ -0,0 +1,12 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile + container_name: backend + ports: + - "5000:5000" + environment: + - FLASK_ENV=development + - FLASK_APP=project + - PYTHONPATH=./ From 22dc0c0021bf5e88e6af2741bd76a8b2ff3ba2fe Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 12:44:49 -0300 Subject: [PATCH 12/23] feat: Add development configuration for database and notification settings --- .secrets.toml | 4 ++++ settings.toml | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/.secrets.toml b/.secrets.toml index ca34fb5..807a782 100644 --- a/.secrets.toml +++ b/.secrets.toml @@ -8,6 +8,10 @@ MERCADOPAGO_ACCESS_TOKEN = "TEST-3070206317287648-031707-c6e11b557ecde6fb4740943 SQLALCHEMY_DATABASE_URI = "postgresql://myuser:mypassword1@postgres:5432/mydatabase" REDIS_URL = "redis://redis:6379" +[development] +SQLALCHEMY_DATABASE_URI = "postgresql://notification_kjwh_user:yaWtOEKOfnT8Go0rpNl2oMGQYVxjXccQ@dpg-cvkm9q15pdvs73bavtpg-a.oregon-postgres.render.com/notification_kjwh" +REDIS_URL = "rediss://red-cus5n0an91rc73di3440:NCMGqJ6UjrX2Xmf1FwQTFU2kmv75q7Xa@oregon-keyvalue.render.com:6379" + [production] SQLALCHEMY_DATABASE_URI = "postgresql://postgres.yajhpyrjyrmhajktpqfr:Hampersoujr24@aws-0-sa-east-1.pooler.supabase.com:5432/postgres" diff --git a/settings.toml b/settings.toml index 9b8a44c..1c43c44 100644 --- a/settings.toml +++ b/settings.toml @@ -28,6 +28,11 @@ FLASK_ENV = "staging" INCLUDES = ["default"] NOTIFICATION_URL = 'https://staging-hamper-backend.onrender.com/api/v1/notification' +[development] +FLASK_ENV = "development" +INCLUDES = ["default"] +NOTIFICATION_URL = 'https://staging-hamper-backend.onrender.com/api/v1/notification' + [production] FLASK_ENV = "production" INCLUDES = ["default"] From 881bad1a4b6d99fe3695b999136ccfc062e6d55b Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Sun, 30 Mar 2025 12:50:31 -0300 Subject: [PATCH 13/23] feat: Add route for notification resource in REST API --- project/ext/restapi/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project/ext/restapi/__init__.py b/project/ext/restapi/__init__.py index c0ff970..86b9b32 100644 --- a/project/ext/restapi/__init__.py +++ b/project/ext/restapi/__init__.py @@ -59,7 +59,8 @@ payment_ns.add_resource(PaymentResourceID, "/") lead_ns.add_resource(LeadResource, "/") -notification_ns.add_resource(NotificationResource) + +notification_ns.add_resource(NotificationResource, "/") api.add_namespace(establishment_ns) api.add_namespace(user_ns) From 1e699350f4e1a187067cb9e3c4253c471f807ee4 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Mon, 31 Mar 2025 15:00:59 -0300 Subject: [PATCH 14/23] fix: Add missing email in clients mock --- scripts/populate_database.py | 72 ++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/scripts/populate_database.py b/scripts/populate_database.py index 064dff4..b650846 100644 --- a/scripts/populate_database.py +++ b/scripts/populate_database.py @@ -11,7 +11,7 @@ "state": "Pernambuco", "city": "Recife", "address": "Rua das Ostras, 123", - "complement": "Ao lado do mercado central" + "complement": "Ao lado do mercado central", }, { "id": 2, @@ -23,7 +23,7 @@ "state": "Alagoas", "city": "Maceió", "address": "Av. Beira Mar, 456", - "complement": "Próximo ao shopping" + "complement": "Próximo ao shopping", }, ] @@ -40,7 +40,7 @@ "has_lactose": True, "is_vegan": False, "is_vegetarian": False, - "establishment_id": 1 + "establishment_id": 1, }, { "id": 2, @@ -53,7 +53,7 @@ "has_lactose": False, "is_vegan": False, "is_vegetarian": False, - "establishment_id": 1 + "establishment_id": 1, }, { "id": 3, @@ -66,7 +66,7 @@ "has_lactose": False, "is_vegan": False, "is_vegetarian": False, - "establishment_id": 2 + "establishment_id": 2, }, { "id": 4, @@ -79,8 +79,8 @@ "has_lactose": False, "is_vegan": False, "is_vegetarian": False, - "establishment_id": 2 - } + "establishment_id": 2, + }, ] @@ -89,40 +89,42 @@ "id": 1, "firstname": "João", "lastname": "Pereira", - "email": "joaopereira@gmail.com" + "email": "joaopereira@gmail.com", }, { "id": 2, "firstname": "Maria", "lastname": "Silva", - "email": "mariasilva@hotmail.com" - } + "email": "mariasilva@hotmail.com", + }, ] mock_clients = [ { "id": 1, - "client_name": "João Pereira", - "client_cellphone": "47999567032", - "client_cpf": "12345678911", - "client_address": "Rua das Palmeiras", - "client_address_number": 67, - "client_address_complement": "Casa verde, ao lado de uma padaria", - "client_address_neighborhood": "Bela Vista", - "client_zip_code": "12345678" + "name": "João Pereira", + "cellphone": "47999567032", + "cpf": "12345678911", + "address": "Rua das Palmeiras", + "address_number": 67, + "address_complement": "Casa verde, ao lado de uma padaria", + "address_neighborhood": "Bela Vista", + "zip_code": "12345678", + "email": "joaopereira@gmail.com", }, { "id": 2, - "client_name": "Maria Silva", - "client_cellphone": "41996314578", - "client_cpf": "12345678910", - "client_address": "Rua Teodoro Sampaio", - "client_address_number": 251, - "client_address_complement": "Casa azul, em frente ao mercado", - "client_address_neighborhood": "Bom Fim", - "client_zip_code": "98765432" - } + "name": "Maria Silva", + "cellphone": "41996314578", + "cpf": "12345678910", + "address": "Rua Teodoro Sampaio", + "address_number": 251, + "address_complement": "Casa azul, em frente ao mercado", + "address_neighborhood": "Bom Fim", + "zip_code": "98765432", + "email": "mariasilva@hotmail.com", + }, ] @@ -132,28 +134,28 @@ "client_id": 1, "establishment_id": 1, "products": [1], - "payment": "Dinheiro" + "payment": "Dinheiro", }, { "id": 2, "client_id": 2, "establishment_id": 2, "products": [3, 4], - "payment": "Pix" - } + "payment": "Pix", + }, ] -url = 'http://127.0.0.1:5000/api/v1/' +url = "http://127.0.0.1:5000/api/v1/" -headers = { - 'Content-Type': 'application/json' -} +headers = {"Content-Type": "application/json"} for establishment in mock_establishments: - response = requests.post(f"{url}/establishments/", json=establishment, headers=headers) + response = requests.post( + f"{url}/establishments/", json=establishment, headers=headers + ) print(response.status_code, "establishment") for product in mock_products: From f253501fe863a5f2f992ef8cdd97e1e7c93fc7b1 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Mon, 31 Mar 2025 15:05:42 -0300 Subject: [PATCH 15/23] feat: Add function to retrieve payment by MercadoPago ID --- project/service/payment_service.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/project/service/payment_service.py b/project/service/payment_service.py index 8208a0a..a134730 100644 --- a/project/service/payment_service.py +++ b/project/service/payment_service.py @@ -1,3 +1,5 @@ +# type: ignore + import logging from http import HTTPStatus from typing import TypedDict @@ -32,6 +34,12 @@ def get_payment(payment_id: int): return payment if (payment := Payment.query.get(payment_id)) else None +def get_payment_by_mercadopago_id(id: int): + existing_payment = Payment.query.filter_by(mercadopago_id=id).first() + + return existing_payment + + def get_all_payments(): return Payment.query.all() From 5bc8d9640cabeb158e6c2852a4c0ba1d7978b556 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Mon, 31 Mar 2025 15:18:47 -0300 Subject: [PATCH 16/23] fix: Change mercadopago_id type from String to Integer in Payment model --- project/models/payment_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/models/payment_model.py b/project/models/payment_model.py index a9532c9..77c1c4e 100644 --- a/project/models/payment_model.py +++ b/project/models/payment_model.py @@ -26,7 +26,7 @@ class Payment(db.Model): qr_code = db.Column(db.String, nullable=True, default=None) qr_code_base64 = db.Column(db.Text, nullable=True, default=None) ticket_url = db.Column(db.String, nullable=True, default=None) - mercadopago_id = db.Column(db.String, nullable=True, default=None) + mercadopago_id = db.Column(db.Integer, nullable=True, default=None) date_of_expiration = db.Column(db.DateTime, nullable=True, default=None) order_id = db.Column(db.ForeignKey("order.id")) order = db.relationship("Order", back_populates="payment") From 574104706d93b054a1e5665c8b5cca4ee0e600de Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Mon, 31 Mar 2025 15:19:23 -0300 Subject: [PATCH 17/23] feat: add Notification model --- project/doc_model/doc_models.py | 43 +++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/project/doc_model/doc_models.py b/project/doc_model/doc_models.py index 3157620..f6f59a8 100644 --- a/project/doc_model/doc_models.py +++ b/project/doc_model/doc_models.py @@ -15,15 +15,23 @@ "Establishment", { "id": fields.Integer(description="ID do estabelecimento"), - "official_name": fields.String(required=True, description="Nome oficial do estabelecimento"), - "fantasy_name": fields.String(required=True, description="Nome fantasia do estabelecimento"), + "official_name": fields.String( + required=True, description="Nome oficial do estabelecimento" + ), + "fantasy_name": fields.String( + required=True, description="Nome fantasia do estabelecimento" + ), "cnpj": fields.String(required=True, description="CNPJ do estabelecimento"), - "telephone": fields.String(required=True, description="Telefone do estabelecimento"), + "telephone": fields.String( + required=True, description="Telefone do estabelecimento" + ), "zip_code": fields.String(required=True, description="CEP do estabelecimento"), "state": fields.String(required=True, description="Estado do estabelecimento"), "city": fields.String(required=True, description="Cidade do estabelecimento"), - "address": fields.String(required=True, description="Endereço do estabelecimento"), - "complement": fields.String(description="Complemento do endereço") + "address": fields.String( + required=True, description="Endereço do estabelecimento" + ), + "complement": fields.String(description="Complemento do endereço"), }, ) @@ -57,7 +65,9 @@ "is_vegetarian": fields.Boolean( required=True, description="Indica se o produto é vegetariano" ), - "establishment_id": fields.Integer(required=True, description="ID do estabelecimento"), + "establishment_id": fields.Integer( + required=True, description="ID do estabelecimento" + ), }, ) @@ -65,7 +75,9 @@ "Order", { "client_id": fields.Integer(required=True, description="ID do cliente"), - "establishment_id": fields.Integer(required=True, description="ID do estabelecimento"), + "establishment_id": fields.Integer( + required=True, description="ID do estabelecimento" + ), "products": fields.List(fields.Integer, description="ID dos produtos"), }, ) @@ -102,3 +114,20 @@ "order_id": fields.Integer(required=True, description="ID do pedido"), }, ) +notification_model = api.model( + "Notification", + { + "action": fields.String(required=True, description="Ação da notificação"), + "api_version": fields.String(required=True, description="Versão da API"), + "data": fields.Raw(required=True, description="Dados da notificação"), + "date_created": fields.String( + required=True, description="Data de criação da notificação" + ), + "id": fields.String(required=True, description="ID da notificação"), + "live_mode": fields.Boolean( + required=True, description="Indica se está em modo live" + ), + "type": fields.String(required=True, description="Tipo da notificação"), + "user_id": fields.Integer(required=True, description="ID do usuário"), + }, +) From a10a986da626ae55ad33407df8a9ebb60e2d2143 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Mon, 31 Mar 2025 16:29:29 -0300 Subject: [PATCH 18/23] feat: update database schema for establishment, payment, and product tables --- migrations/versions/dbd49da0c706_.py | 72 ++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 migrations/versions/dbd49da0c706_.py diff --git a/migrations/versions/dbd49da0c706_.py b/migrations/versions/dbd49da0c706_.py new file mode 100644 index 0000000..0caf5b8 --- /dev/null +++ b/migrations/versions/dbd49da0c706_.py @@ -0,0 +1,72 @@ +"""empty message + +Revision ID: dbd49da0c706 +Revises: 9255441d2a22 +Create Date: 2025-03-31 16:29:09.273131 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'dbd49da0c706' +down_revision = '9255441d2a22' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('establishment', schema=None) as batch_op: + batch_op.alter_column('official_name', + existing_type=sa.VARCHAR(length=40), + type_=sa.String(length=45), + existing_nullable=False) + batch_op.alter_column('fantasy_name', + existing_type=sa.VARCHAR(length=40), + type_=sa.String(length=45), + existing_nullable=False) + batch_op.create_unique_constraint(None, ['telephone']) + + with op.batch_alter_table('payment', schema=None) as batch_op: + batch_op.alter_column('mercadopago_id', + existing_type=sa.VARCHAR(), + type_=sa.Integer(), + existing_nullable=True) + + with op.batch_alter_table('product', schema=None) as batch_op: + batch_op.alter_column('value', + existing_type=sa.REAL(), + type_=sa.Float(precision=6), + existing_nullable=False) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('product', schema=None) as batch_op: + batch_op.alter_column('value', + existing_type=sa.Float(precision=6), + type_=sa.REAL(), + existing_nullable=False) + + with op.batch_alter_table('payment', schema=None) as batch_op: + batch_op.alter_column('mercadopago_id', + existing_type=sa.Integer(), + type_=sa.VARCHAR(), + existing_nullable=True) + + with op.batch_alter_table('establishment', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='unique') + batch_op.alter_column('fantasy_name', + existing_type=sa.String(length=45), + type_=sa.VARCHAR(length=40), + existing_nullable=False) + batch_op.alter_column('official_name', + existing_type=sa.String(length=45), + type_=sa.VARCHAR(length=40), + existing_nullable=False) + + # ### end Alembic commands ### From 5a4f2a8da099f61d37b5f3ece2c66f489be0713f Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Mon, 31 Mar 2025 16:43:46 -0300 Subject: [PATCH 19/23] feat: add client, establishment, lead, user, order, product, and payment tables with relationships --- migrations/env.py | 9 ++- migrations/versions/dbd49da0c706_.py | 72 ------------------- .../{9255441d2a22_.py => fbcd61f123d1_.py} | 15 ++-- 3 files changed, 14 insertions(+), 82 deletions(-) delete mode 100644 migrations/versions/dbd49da0c706_.py rename migrations/versions/{9255441d2a22_.py => fbcd61f123d1_.py} (94%) diff --git a/migrations/env.py b/migrations/env.py index 89f80b2..4c97092 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -19,7 +19,7 @@ def get_engine(): try: # this works with Flask-SQLAlchemy<3 and Alchemical return current_app.extensions['migrate'].db.get_engine() - except TypeError: + except (TypeError, AttributeError): # this works with Flask-SQLAlchemy>=3 return current_app.extensions['migrate'].db.engine @@ -90,14 +90,17 @@ def process_revision_directives(context, revision, directives): directives[:] = [] logger.info('No changes in schema detected.') + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + connectable = get_engine() with connectable.connect() as connection: context.configure( connection=connection, target_metadata=get_metadata(), - process_revision_directives=process_revision_directives, - **current_app.extensions['migrate'].configure_args + **conf_args ) with context.begin_transaction(): diff --git a/migrations/versions/dbd49da0c706_.py b/migrations/versions/dbd49da0c706_.py deleted file mode 100644 index 0caf5b8..0000000 --- a/migrations/versions/dbd49da0c706_.py +++ /dev/null @@ -1,72 +0,0 @@ -"""empty message - -Revision ID: dbd49da0c706 -Revises: 9255441d2a22 -Create Date: 2025-03-31 16:29:09.273131 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = 'dbd49da0c706' -down_revision = '9255441d2a22' -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('establishment', schema=None) as batch_op: - batch_op.alter_column('official_name', - existing_type=sa.VARCHAR(length=40), - type_=sa.String(length=45), - existing_nullable=False) - batch_op.alter_column('fantasy_name', - existing_type=sa.VARCHAR(length=40), - type_=sa.String(length=45), - existing_nullable=False) - batch_op.create_unique_constraint(None, ['telephone']) - - with op.batch_alter_table('payment', schema=None) as batch_op: - batch_op.alter_column('mercadopago_id', - existing_type=sa.VARCHAR(), - type_=sa.Integer(), - existing_nullable=True) - - with op.batch_alter_table('product', schema=None) as batch_op: - batch_op.alter_column('value', - existing_type=sa.REAL(), - type_=sa.Float(precision=6), - existing_nullable=False) - - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table('product', schema=None) as batch_op: - batch_op.alter_column('value', - existing_type=sa.Float(precision=6), - type_=sa.REAL(), - existing_nullable=False) - - with op.batch_alter_table('payment', schema=None) as batch_op: - batch_op.alter_column('mercadopago_id', - existing_type=sa.Integer(), - type_=sa.VARCHAR(), - existing_nullable=True) - - with op.batch_alter_table('establishment', schema=None) as batch_op: - batch_op.drop_constraint(None, type_='unique') - batch_op.alter_column('fantasy_name', - existing_type=sa.String(length=45), - type_=sa.VARCHAR(length=40), - existing_nullable=False) - batch_op.alter_column('official_name', - existing_type=sa.String(length=45), - type_=sa.VARCHAR(length=40), - existing_nullable=False) - - # ### end Alembic commands ### diff --git a/migrations/versions/9255441d2a22_.py b/migrations/versions/fbcd61f123d1_.py similarity index 94% rename from migrations/versions/9255441d2a22_.py rename to migrations/versions/fbcd61f123d1_.py index 29f69b6..effcf98 100644 --- a/migrations/versions/9255441d2a22_.py +++ b/migrations/versions/fbcd61f123d1_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 9255441d2a22 +Revision ID: fbcd61f123d1 Revises: -Create Date: 2025-03-28 14:45:25.748190 +Create Date: 2025-03-31 16:33:11.681985 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '9255441d2a22' +revision = 'fbcd61f123d1' down_revision = None branch_labels = None depends_on = None @@ -36,8 +36,8 @@ def upgrade(): op.create_table('establishment', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), - sa.Column('official_name', sa.String(length=40), nullable=False), - sa.Column('fantasy_name', sa.String(length=40), nullable=False), + sa.Column('official_name', sa.String(length=45), nullable=False), + sa.Column('fantasy_name', sa.String(length=45), nullable=False), sa.Column('cnpj', sa.String(length=14), nullable=False), sa.Column('telephone', sa.String(length=11), nullable=False), sa.Column('zip_code', sa.String(length=9), nullable=False), @@ -48,7 +48,8 @@ def upgrade(): sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('cnpj'), sa.UniqueConstraint('fantasy_name'), - sa.UniqueConstraint('official_name') + sa.UniqueConstraint('official_name'), + sa.UniqueConstraint('telephone') ) op.create_table('lead', sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), @@ -108,7 +109,7 @@ def upgrade(): sa.Column('qr_code', sa.String(), nullable=True), sa.Column('qr_code_base64', sa.Text(), nullable=True), sa.Column('ticket_url', sa.String(), nullable=True), - sa.Column('mercadopago_id', sa.String(), nullable=True), + sa.Column('mercadopago_id', sa.Integer(), nullable=True), sa.Column('date_of_expiration', sa.DateTime(), nullable=True), sa.Column('order_id', sa.Integer(), nullable=True), sa.ForeignKeyConstraint(['order_id'], ['order.id'], ), From 88826ef892aa5c22536e10878632c8db04791e03 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Mon, 31 Mar 2025 16:44:59 -0300 Subject: [PATCH 20/23] feat: enhance Notification resource to handle payment updates and improve error handling --- project/controller/notification_controller.py | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/project/controller/notification_controller.py b/project/controller/notification_controller.py index bec8c6a..ed6607d 100644 --- a/project/controller/notification_controller.py +++ b/project/controller/notification_controller.py @@ -1,18 +1,65 @@ -from click import echo from flask import request from flask_restx import Resource +from project.doc_model.doc_models import api, notification_model +from project.ext.payment_gateways.mercadopago.schema import NotificationPayload +from project.ext.serializer import PaymentSchema +from project.service.payment_service import ( + get_payment_by_mercadopago_id, + update_payment, +) + +from ..ext.payment_gateways.mercadopago.payment import get_payment as get_mp_payment + +payment_schema = PaymentSchema(many=False) + + +# { +# "action": "payment.updated", +# "api_version": "v1", +# "data": {"id": "1334092911"}, +# "date_created": "2021-11-01T02:02:02Z", +# "id": "1334092911", +# "live_mode": false, +# "type": "payment", +# "user_id": 221150409, +# } + class NotificationResource(Resource): + @api.expect(notification_model) def post(self): try: - notification_data = request.json + if not request.json: + return {"error": "Invalid request json"}, 400 + + notification_data: NotificationPayload = request.json + if not isinstance(notification_data, dict): - return {"error": "Invalid input"}, 400 + return {"error": "Invalid input data type"}, 400 + + if notification_data["action"] == "payment.updated": + mercadopago_id = notification_data["id"] + + existing_payment = get_payment_by_mercadopago_id(id=int(mercadopago_id)) + + if not existing_payment: + return { + "error": "Nenhum pagamento encontrado para essa notificação!" + }, 404 + + mp_payment = get_mp_payment(id=int(mercadopago_id)) + actual_status = mp_payment["response"]["status"] + + updated_payment = update_payment( + id=existing_payment.id, + updated_data={"status": actual_status}, + ) - echo(notification_data) + if not updated_payment: + return {"error": "Pagamento não atualizado"}, 400 - return {}, 200 + return {"messaage": "Pagaamento atualizado com sucesso"}, 200 except Exception as e: return {"error": str(e)}, 400 From 87d127c59006250400363367d5326daf3ddfde8a Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Tue, 1 Apr 2025 10:13:22 -0300 Subject: [PATCH 21/23] feat: enhance Notification endpoint to process payment updates with detailed responses and example payload --- project/controller/notification_controller.py | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/project/controller/notification_controller.py b/project/controller/notification_controller.py index ed6607d..8680ee1 100644 --- a/project/controller/notification_controller.py +++ b/project/controller/notification_controller.py @@ -14,21 +14,42 @@ payment_schema = PaymentSchema(many=False) -# { -# "action": "payment.updated", -# "api_version": "v1", -# "data": {"id": "1334092911"}, -# "date_created": "2021-11-01T02:02:02Z", -# "id": "1334092911", -# "live_mode": false, -# "type": "payment", -# "user_id": 221150409, -# } - - class NotificationResource(Resource): @api.expect(notification_model) + @api.response(200, "Pagamento atualizado com sucesso") + @api.response(400, "Requisição inválida ou erro interno") + @api.response(404, "Pagamento não encontrado") + @api.doc( + description=""" + Endpoint para processar notificações do MercadoPago sobre atualizações de pagamento. + + Recebe um payload no formato específico do MercadoPago e atualiza o status do pagamento correspondente. + """, + params={}, + body=notification_model, + ) def post(self): + """ + Processa notificações de atualização de pagamento. + + Exemplo de requisição (JSON): + { + "action": "payment.updated", + "api_version": "v1", + "data": {"id": "12345"}, + "date_created": "2023-01-01T00:00:00Z", + "id": "12345", + "live_mode": false, + "type": "payment", + "user_id": 123456 + } + + Respostas: + - 200: Pagamento atualizado (ex: {"message": "Pagamento atualizado com sucesso"}), + - 400: Erro na requisição (ex: JSON inválido, dados incorretos), + - 404: Pagamento não encontrado no sistema + """ + try: if not request.json: return {"error": "Invalid request json"}, 400 From 540944b9500c2d58b1b46b4d83476e1513c29c96 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Tue, 1 Apr 2025 11:15:09 -0300 Subject: [PATCH 22/23] feat: add docker-compose configuration for backend service --- ...r-compose.ext.yml => docker-compose.backend.yml | 4 +++- docker-compose.yml | 14 -------------- 2 files changed, 3 insertions(+), 15 deletions(-) rename docker-compose.ext.yml => docker-compose.backend.yml (79%) diff --git a/docker-compose.ext.yml b/docker-compose.backend.yml similarity index 79% rename from docker-compose.ext.yml rename to docker-compose.backend.yml index 4b615a9..e33a766 100644 --- a/docker-compose.ext.yml +++ b/docker-compose.backend.yml @@ -7,6 +7,8 @@ services: ports: - "5000:5000" environment: - - FLASK_ENV=development + - FLASK_ENV=staging - FLASK_APP=project - PYTHONPATH=./ + volumes: + - .:/app \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index b9ecbfc..ff4d07f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,4 @@ services: - backend: - build: - context: . - dockerfile: Dockerfile - container_name: backend - ports: - - "5000:5000" - environment: - - FLASK_ENV=staging - - FLASK_APP=project - - PYTHONPATH=./ - volumes: - - .:/app postgres: image: postgres:14 container_name: postgres-container @@ -29,6 +16,5 @@ services: container_name: redis ports: - "6379:6379" - volumes: pgdata: \ No newline at end of file From a995a2c7ff408a4b731d648cbcb9847dcaede3e5 Mon Sep 17 00:00:00 2001 From: bentoluizv Date: Tue, 1 Apr 2025 11:15:14 -0300 Subject: [PATCH 23/23] docs: update README with installation instructions and Docker usage details --- README.md | 169 ++++++++++++++---------------------------------------- 1 file changed, 43 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index 9779c7b..947d196 100644 --- a/README.md +++ b/README.md @@ -6,182 +6,99 @@ Este é o backend do projeto DeliveryAPP. Ele é construído usando Flask e forn TODO: Documentar estrutura de pastas e responsabilidades. -## Configuração - -### Requisitos +## Requisitos - Python 3.11 - PostgreSQL - Redis -### Instalação - -1. Clone o repositório: +## Clone o repositório -```bash +```bash= git clone https://github.com/seuusuario/delivery-app-backend.git cd delivery-app-backend ``` -2. Crie um ambiente virtual e ative-o: +## Rodando Local -```bash +> A aplicação não está rodando na branch main e sim em homologação, não esqueça de fazer: +> `git checkout homologacao` + +1. Crie um ambiente virtual e ative-o: + +```bash= python -m venv venv source venv/bin/activate # No Windows use `venv\Scripts\activate` ``` -3. Instale as dependências: +2. Instale as dependências: -```bash +```bash= pip install -r requirements.txt ``` -4. Configure as variáveis de ambiente: +3. Configure as variáveis de ambiente: -```bash +```bash= export FLASK_APP=project export FLASK_ENV=local (local | staging | production) ``` -5. Rode a aplicação: - -```bash -docker compose up -flask run -``` - -9. Inicialize o banco de dados: - -```bash -flask db init -``` - -10. Migrações - -```bash -# para criar uma nova migração -flask db migrate -# para atualizar o banco para a nova migração -flask db upgrade -``` - -Popule o banco de dados com dados iniciais: - -```bash -python scripts/populate_database.py -``` - -## Executando a Aplicação +4. Rode a aplicação: -### TL:DR - -Para executar a aplicação localmente, use o seguinte comando: - -```bash -export FLASK_ENV = local -docker compose up -flask run +```bash= +docker compose up # Sobe apenas o banco de dados e redis. +flask run # Roda a aplicação em modo desenvolvimento. ``` -### Docker - -#### Dockerfile - -```bash -FROM python:3.11-slim AS builder - -WORKDIR /app +## Rodando com Docker -COPY requirements.txt . +Existem dois arquivos docker-compose no projeto. O docker-compose.yml roda apenas a infra e o docker-compose.backend.yml que sobe o backend. -RUN pip install --upgrade pip && \ - pip install --user --no-cache-dir -r requirements.txt +Isso é util para subir a aplicação em modo produção antes de enviar para homologação. -FROM python:3.11-slim - -WORKDIR /app - -COPY --from=builder /root/.local /root/.local - -COPY . . - -ENV PATH=/root/.local/bin:$PATH - -EXPOSE 5000 - -CMD ["gunicorn","-w", "4", "-b", "0.0.0.0:5000", "project:create_app()"] +Você pode rodar multiplos arquivos compose adicionando a tag -f. +```bash= +docker-compose -f docker-compose.yml -f docker-compose.backend.yml down ``` -#### Docker Compose - -1. Arquivo compose com a aplicação, caso FLASK_ENV for "local" você deve subir os serviços com o outro docker-compose.yml listado abaixo +## Banco de dados e Migrações -```bash -services: - backend: - build: - context: . - dockerfile: Dockerfile - container_name: backend - ports: - - "5000:5000" - environment: - - FLASK_ENV=local - - FLASK_APP=project - volumes: - - .:/app -``` - -2. Compose com a infra, para rodar o ambiente externo localmente, você deve definir as váriaveis de ambiente para local. Para testar com a build que vai para homologação podemos usar em conjunto com o **docker-compose.ext.yml** - -```bashs -services: - postgres: - image: postgres:14 - container_name: postgres-container - environment: - POSTGRES_DB: mydatabase - POSTGRES_USER: myuser - POSTGRES_PASSWORD: mypassword1 - ports: - - "5432:5432" - volumes: - - pgdata:/var/lib/postgresql/data - - redis: - image: redis:latest - container_name: redis - ports: - - "6379:6379" - -volumes: - pgdata: -``` +```bash= +# Inicia a conexão com o banco de dados. +flask db init -para rodar a aplicação local edite FLASK_ENV para **local** e rode o comando: +# Para criar uma nova migração +flask db migrate -m "Pequena descrição sobre o que a nova migração faz" -```bash -docker compose -f docker-compose.yml -f docker-compose.ext.yml +# Para atualizar o banco para a nova migração +flask db upgrade ``` -para rodar o ambiente de homologação mude o FLASK_ENV para **staging** e rode: +Popule o banco de dados com dados iniciais: ```bash -docker compose -f docker-compose.ext.yml up +python scripts/populate_database.py ``` -Você perceberá que ao tentara executar com FLASK_ENV como **production** você receberá um erro. Isso acontece pois não é possível se conectar ao banco de dados e cache diretamente de uma maquina local. Essa configuração deverá ser usada apenas pelo host da aplicação. - ## Ambiente -A aplicação usa Dynaconf para gerenciamento de configuração. A configuração é definida no arquivo `settings.toml`. +A aplicação usa Dynaconf para gerenciamento de configuração. A configuração é definida no arquivo `settings.toml` e `.secrets.toml`. ### Variáveis de Ambiente - `FLASK_APP`: O nome da aplicação Flask. - `FLASK_ENV`: O ambiente em que a aplicação está sendo executada (local | staging | production). +## Documentação da API + +TODO + +## Testes + +TODO + ## Contato Para quaisquer perguntas ou problemas, entre em contato com os mantenedores do projeto.