Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0329997
Chore: Add NotificationPayload TypedDict to schema.py for MercadoPago…
bentoluizv Mar 28, 2025
546d828
Chore: Update database and Redis URLs in settings.toml for containeri…
bentoluizv Mar 30, 2025
13d3be7
Merge branch 'homologacao' into feature/notificacao-pagamentos-mercad…
bentoluizv Mar 30, 2025
609afc6
feat: Add notification endpoint to REST API
bentoluizv Mar 30, 2025
339d403
feat: Change command to run Flask application in Dockerfile for test …
bentoluizv Mar 30, 2025
c54c0b0
feat: Log payload in notification endpoint for debugging purposes
bentoluizv Mar 30, 2025
554b2b5
feat: Log request headers in notification endpoint for improved debug…
bentoluizv Mar 30, 2025
8e035f4
feat: Update database and Redis connection strings in secrets configu…
bentoluizv Mar 30, 2025
5f1e8d5
feat: Restore gunicorn command in Dockerfile for production deployment
bentoluizv Mar 30, 2025
3f43d46
feat: Refactor namespace definitions for improved readability and con…
bentoluizv Mar 30, 2025
b9ff26d
feat: Implement notification endpoint for handling incoming notificat…
bentoluizv Mar 30, 2025
1d9f9cb
feat: Add Docker Compose configuration for backend service
bentoluizv Mar 30, 2025
22dc0c0
feat: Add development configuration for database and notification set…
bentoluizv Mar 30, 2025
881bad1
feat: Add route for notification resource in REST API
bentoluizv Mar 30, 2025
1e69935
fix: Add missing email in clients mock
bentoluizv Mar 31, 2025
f253501
feat: Add function to retrieve payment by MercadoPago ID
bentoluizv Mar 31, 2025
5bc8d96
fix: Change mercadopago_id type from String to Integer in Payment model
bentoluizv Mar 31, 2025
5741047
feat: add Notification model
bentoluizv Mar 31, 2025
a10a986
feat: update database schema for establishment, payment, and product …
bentoluizv Mar 31, 2025
5a4f2a8
feat: add client, establishment, lead, user, order, product, and paym…
bentoluizv Mar 31, 2025
88826ef
feat: enhance Notification resource to handle payment updates and imp…
bentoluizv Mar 31, 2025
87d127c
feat: enhance Notification endpoint to process payment updates with d…
bentoluizv Apr 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .secrets.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

12 changes: 12 additions & 0 deletions docker-compose.ext.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile
container_name: backend
ports:
- "5000:5000"
environment:
- FLASK_ENV=development
- FLASK_APP=project
- PYTHONPATH=./
9 changes: 6 additions & 3 deletions migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""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
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '9255441d2a22'
revision = 'fbcd61f123d1'
down_revision = None
branch_labels = None
depends_on = None
Expand All @@ -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),
Expand All @@ -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),
Expand Down Expand Up @@ -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'], ),
Expand Down
15 changes: 1 addition & 14 deletions project/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"""

from dynaconf import FlaskDynaconf
from flask import Flask, request
from flask import Flask
from flask_cors import CORS


Expand All @@ -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={
Expand All @@ -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!")
Expand All @@ -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)
86 changes: 86 additions & 0 deletions project/controller/notification_controller.py
Original file line number Diff line number Diff line change
@@ -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
43 changes: 36 additions & 7 deletions project/doc_model/doc_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
)

Expand Down Expand Up @@ -57,15 +65,19 @@
"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"
),
},
)

order_model = api.model(
"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"),
},
)
Expand Down Expand Up @@ -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"),
},
)
11 changes: 11 additions & 0 deletions project/ext/payment_gateways/mercadopago/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions project/ext/restapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -55,13 +60,16 @@

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)
api.add_namespace(client_ns)
api.add_namespace(order_ns)
api.add_namespace(payment_ns)
api.add_namespace(lead_ns)
api.add_namespace(notification_ns)


def init_app(app):
Expand All @@ -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)
2 changes: 1 addition & 1 deletion project/models/payment_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Loading