diff --git a/.secrets.toml b/.secrets.toml index f1f1546..807a782 100644 --- a/.secrets.toml +++ b/.secrets.toml @@ -5,7 +5,11 @@ 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" +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" diff --git a/Dockerfile b/Dockerfile index cd12970..b8b5598 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,4 +20,5 @@ 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"] 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=./ 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/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'], ), 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..8680ee1 --- /dev/null +++ b/project/controller/notification_controller.py @@ -0,0 +1,86 @@ +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) + + +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 + + notification_data: NotificationPayload = request.json + + if not isinstance(notification_data, dict): + 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}, + ) + + if not updated_payment: + return {"error": "Pagamento não atualizado"}, 400 + + return {"messaage": "Pagaamento atualizado com sucesso"}, 200 + + except Exception as e: + return {"error": str(e)}, 400 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"), + }, +) 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 diff --git a/project/ext/restapi/__init__.py b/project/ext/restapi/__init__.py index 07e0250..86b9b32 100644 --- a/project/ext/restapi/__init__.py +++ b/project/ext/restapi/__init__.py @@ -4,28 +4,33 @@ 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, + notification_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.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 -from ...controller.establishment_controller import EstablishmentResource, EstablishmentResourceID establishment_ns.models["EstablishmentModel"] = establishment_model user_ns.models["UserModel"] = user_model @@ -55,6 +60,8 @@ lead_ns.add_resource(LeadResource, "/") +notification_ns.add_resource(NotificationResource, "/") + api.add_namespace(establishment_ns) api.add_namespace(user_ns) api.add_namespace(product_ns) @@ -62,6 +69,7 @@ api.add_namespace(order_ns) api.add_namespace(payment_ns) api.add_namespace(lead_ns) +api.add_namespace(notification_ns) def init_app(app): @@ -73,3 +81,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) 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") 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() 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" ) 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: diff --git a/settings.toml b/settings.toml index 693784d..1c43c44 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] @@ -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"]