├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── README.pt-br.md ├── docker ├── compose.yaml ├── kong │ ├── POSTGRES_PASSWORD │ ├── compose.yaml │ └── config │ │ └── kong.yaml ├── rabbitmq │ ├── enabled_plugins │ └── rabbitmq.conf ├── scripts │ ├── manage.sh │ ├── run.sh │ ├── start.sh │ └── stop.sh └── services │ ├── order │ ├── .dockerignore │ ├── .editorconfig │ ├── .env.example │ ├── .gitignore │ ├── .pre-commit-config.yaml │ ├── Dockerfile │ ├── LICENSE │ ├── README.md │ ├── __init__.py │ ├── compose.yaml │ ├── gunicorn.conf.py │ ├── manage.py │ ├── poetry.lock │ ├── pyproject.toml │ ├── requirements.txt │ ├── src │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── callback.py │ │ │ ├── fixtures │ │ │ │ └── order.json │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── serializers.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── tests │ │ └── __init__.py │ ├── payment │ ├── .dockerignore │ ├── .editorconfig │ ├── .env.example │ ├── .gitignore │ ├── .pre-commit-config.yaml │ ├── Dockerfile │ ├── LICENSE │ ├── README.md │ ├── __init__.py │ ├── compose.yaml │ ├── gunicorn.conf.py │ ├── manage.py │ ├── poetry.lock │ ├── pyproject.toml │ ├── requirements.txt │ ├── src │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── admin.py │ │ │ ├── apps.py │ │ │ ├── callback.py │ │ │ ├── fixtures │ │ │ │ └── payment.json │ │ │ ├── migrations │ │ │ │ ├── 0001_initial.py │ │ │ │ └── __init__.py │ │ │ ├── models.py │ │ │ ├── serializers.py │ │ │ ├── urls.py │ │ │ └── views.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── tests │ │ └── __init__.py │ └── stock │ ├── .dockerignore │ ├── .editorconfig │ ├── .env.example │ ├── .gitignore │ ├── .pre-commit-config.yaml │ ├── Dockerfile │ ├── LICENSE │ ├── README.md │ ├── __init__.py │ ├── compose.yaml │ ├── gunicorn.conf.py │ ├── manage.py │ ├── poetry.lock │ ├── pyproject.toml │ ├── requirements.txt │ ├── src │ ├── __init__.py │ ├── asgi.py │ ├── core │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── callback.py │ │ ├── fixtures │ │ │ ├── payment.json │ │ │ └── stock.json │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── urls.py │ │ └── views.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py │ └── tests │ └── __init__.py ├── docs ├── architecture.excalidraw ├── architecture.png ├── flow.png ├── microservice_action.gif └── saga.postman_collection.json ├── k8s ├── k3d │ └── README.md ├── kong │ ├── README.md │ ├── kong-gateway.yaml │ └── values.yaml ├── rabbitmq │ └── rabbitmq.yaml ├── saga │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── configmap.yaml │ │ ├── deployments.yaml │ │ ├── httproute.yaml │ │ ├── secret.yaml │ │ └── services.yaml │ └── values.yaml ├── services │ ├── order │ │ └── values.yaml │ ├── payment │ │ └── values.yaml │ └── stock │ │ └── values.yaml └── setup.sh └── web ├── .gitignore ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── public ├── next.svg └── vercel.svg ├── src └── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── page.module.css │ └── page.tsx └── tsconfig.json /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 'v0.1.14' 4 | hooks: 5 | - id: ruff 6 | args: [ --fix ] 7 | - id: ruff-format -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Hugo Brilhante 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌐 Saga with Outbox Pattern: Orchestrating Distributed Transactions in Microservices 2 | [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) 3 | [![pt-br](https://img.shields.io/badge/lang-pt--br-green.svg)](README.pt-br.md) 4 | 5 | This repository demonstrates the Outbox Pattern in microservices, leveraging the Django Outbox Pattern library developed 6 | at [@juntossomosmais](https://github.com/juntossomosmais/django-outbox-pattern). 7 | 8 | ### 🎭 Scenario: E-Commerce System 9 | 10 | An e-commerce system uses microservices (Order, Stock, and Payment) to manage orders, stock, and payments. The Saga 11 | pattern is implemented using the Outbox pattern for consistent communication. 12 | 13 | * **📦 Order Service:** 14 | - Receives and processes customer orders. 15 | - Creates order records in the database upon order reception. 16 | 17 | * **📦 Stock Service:** 18 | - Manages product stock. 19 | - Receives an order message to reserve products in stock. 20 | - Confirms reservation, updates the database, and records a message in the Outbox table. 21 | 22 | * **💳 Payment Service:** 23 | - Processes order payments. 24 | - Receives an order message for payment authorization. 25 | - Validates payment, authorizes it, updates the database, and records a message in the Outbox table. 26 | 27 | ### ⚙️ Execution Flow: 28 | 29 | 1. Customer places an order through the Order service. 30 | 2. Order service creates a record in the Outbox table with order details. 31 | 3. Message is sent to the Stock service to reserve products. 32 | 4. Stock service confirms reservation, updates its database, and records a message in the Outbox table. 33 | 5. Message is sent to the Payment service for payment authorization. 34 | 6. Payment service validates payment, authorizes it, updates its database, and records a message in the Outbox table. 35 | 7. Order service periodically checks the Outbox table to process pending messages. 36 | 8. If successful, the order is marked as confirmed, and the customer is notified. 37 | 38 | ![Flow](docs/flow.png) 39 | 40 | ### 🏗️ Infrastructure 41 | 42 | This repository provides configuration files for deploying three Django services (Order, Stock, Payment) on Kubernetes 43 | and Docker Compose. Each service has its PostgreSQL database, and RabbitMQ facilitates communication. Kong serves as an 44 | API gateway and microservices management layer. 45 | 46 | ![Architecture](docs/architecture.png) 47 | 48 | ### 🛠️ Technologies Used 49 | 50 | 1. **Django:** A web framework for rapid Python application development. 51 | 2. **PostgreSQL:** A robust relational database management system. 52 | 3. **RabbitMQ:** Supports asynchronous communication between services. 53 | 4. **Kubernetes:** Container orchestration for automating deployment and scaling. 54 | 5. **Docker Compose:** Simplifies managing multi-container Docker applications. 55 | 6. **Kong:** An API gateway and microservices management layer. 56 | 57 | ## 🚀 Usage Instructions with Docker 58 | 59 | ### 🏁 Starting the Project 60 | 61 | 1. Navigate to the [docker](docker) directory. 62 | ```bash 63 | cd docker 64 | ``` 65 | 66 | 2. Run the start script: 67 | 68 | ```bash 69 | ./scripts/start.sh 70 | ``` 71 | 72 | 3. Access services via: 73 | - Order Admin: [http://localhost:8000/admin](http://localhost:8000/admin) 74 | - Stock Admin: [http://localhost:8001/admin](http://localhost:8001/admin) 75 | - Payment Admin: [http://localhost:8002/admin](http://localhost:8002/admin) 76 | - API: [http://localhost:8080](http://localhost:8080) 77 | - Kong Admin: [http://localhost:8082](http://localhost:8082) 78 | - RabbitMQ Management UI: [http://localhost:15672](http://localhost:15672) 79 | 80 | 4. Use these credentials: 81 | - Django Admin: admin/admin 82 | - RabbitMQ: guest/guest 83 | 84 | ### 🛑 Stopping the Project 85 | 86 | 1. Navigate to project root. 87 | 88 | 2. Run stop script: 89 | 90 | ```bash 91 | ./scripts/stop.sh 92 | ``` 93 | 94 | ## 🚀 Usage Instructions with Kubernetes 95 | 96 | This guide will walk you through setting up a Kubernetes cluster using k3d. Make sure you have Docker installed on your 97 | system before proceeding. 98 | 99 | ### Kubernetes Cluster Setup 100 | 101 | To set up the Kubernetes cluster, follow these steps: 102 | 103 | 1. Navigate to the [k8s](k8s) directory. 104 | ```bash 105 | cd k8s 106 | ``` 107 | 2. Run the `setup.sh` script. 108 | ```bash 109 | ./setup.sh 110 | ``` 111 | 112 | This script will automatically: 113 | 114 | 🚀 Install k3d, kubectl, and Helm if not already installed. 115 | 116 | 🌟 Create a k3d cluster named "saga" with port mapping for load balancing. 117 | 118 | After running the script, your Kubernetes cluster will be set up and ready to use. 119 | 120 | ### Install the Kong Ingress Controller 121 | 122 | 1. **Install the Gateway API CRDs before installing Kong Ingress Controller.** 123 | 124 | ```bash 125 | kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml 126 | ``` 127 | 128 | 2. **Create a Gateway and GatewayClass instance to use.** 129 | 130 | ```bash 131 | kubectl apply -f kong/kong-gateway.yaml 132 | ``` 133 | 134 | 3. **Helm Chart Installation** 135 | 136 | 1. Add the Kong Helm charts: 137 | ```bash 138 | helm repo add kong https://charts.konghq.com 139 | ``` 140 | 2. Update repo: 141 | ```bash 142 | helm repo update 143 | ``` 144 | 3. Install Kong Ingress Controller and Kong Gateway with Helm: 145 | ```bash 146 | helm install kong kong/ingress -n kong --create-namespace --values kong/values.yaml 147 | ``` 148 | 149 | 4. **Verify Installation** 150 | 151 | After installation, ensure that Kong Ingress Controller pods are running: 152 | 153 | ```bash 154 | curl -i 'localhost:8080' 155 | ``` 156 | 157 | The results should look like this: 158 | ```bash 159 | HTTP/1.1 404 Not Found 160 | Date: Sun, 28 Jan 2024 19:14:45 GMT 161 | Content-Type: application/json; charset=utf-8 162 | Connection: keep-alive 163 | Content-Length: 103 164 | X-Kong-Response-Latency: 0 165 | Server: kong/3.5.0 166 | X-Kong-Request-Id: fa55be13bee8575984a67514efbe224c 167 | 168 | { 169 | "message":"no Route matched with those values", 170 | "request_id":"fa55be13bee8575984a67514efbe224c" 171 | } 172 | ``` 173 | **Note:** 174 | > If you encounter `curl: (52) Empty reply from server`, please wait a moment and try again. 175 | 176 | 177 | 5. **Create a RabbitMQ Cluster Kubernetes Operator.** 178 | 179 | 1. Install the RabbitMQ Cluster Operator: 180 | ```bash 181 | kubectl rabbitmq install-cluster-operator 182 | ``` 183 | 2. Create a RabbitMQ cluster: 184 | ```bash 185 | kubectl apply -f rabbitmq/rabbitmq.yaml 186 | ``` 187 | 3. Create a saga exchange: 188 | ```bash 189 | kubectl exec svc/rabbitmq -c rabbitmq -- rabbitmqadmin declare exchange name=saga type=topic -u guest -p guest 190 | ``` 191 | The results should look like this: 192 | ```bash 193 | exchange declared 194 | ``` 195 | **Note:** 196 | > RabbitMQ cluster should be running 197 | 4. Access The Management UI (optional): 198 | ```bash 199 | kubectl rabbitmq manage rabbitmq 200 | ``` 201 | 202 | ### Installing order, stock and payment using Helm 📊 203 | 204 | After setting up the Kubernetes cluster and installing the Kong Ingress Controller: 205 | 206 | 1. Use Helm to create the "order", "stock", and "payment" releases using the Saga chart and corresponding values: 207 | 208 | ```bash 209 | helm install order ./saga --values services/order/values.yaml 210 | helm install stock ./saga --values services/stock/values.yaml 211 | helm install payment ./saga --values services/payment/values.yaml 212 | ``` 213 | 214 | This creates three Helm releases, "order", "stock", and "payment", with configurations specified in their 215 | respective `values.yaml` files. 216 | 217 | Please note that each command creates a specific Helm release with its own configurations. 218 | 219 | ### 🛑 Stopping the Project 220 | 221 | 1. Run cluster delete command: 222 | 223 | ```bash 224 | k3d cluster delete saga 225 | ``` 226 | ## Web App 227 | 228 | ![Web](docs/microservice_action.gif) 229 | 230 | ## 🧪 Testing Scenarios with Postman Collection 231 | 232 | 1. Install [Postman](https://www.postman.com/downloads/). 233 | 234 | 2. Import the Postman [collection](docs/saga.postman_collection.json). 235 | 236 | 3. Collection contains scenarios: 237 | - **Unreserved Stock:** Create order with quantity > 10. 238 | - **Denied Payment:** Create order with amount > $1000. 239 | 240 | 4. Run requests to observe system behavior. -------------------------------------------------------------------------------- /README.pt-br.md: -------------------------------------------------------------------------------- 1 | # 🌐 Saga com Padrão Outbox: Orquestrando Transações Distribuídas em Microsserviços 2 | [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) 3 | [![pt-br](https://img.shields.io/badge/lang-pt--br-green.svg)](README.pt-br.md) 4 | 5 | Este repositório demonstra o Padrão Outbox em microsserviços, aproveitando a biblioteca Django Outbox Pattern desenvolvida em [@juntossomosmais](https://github.com/juntossomosmais/django-outbox-pattern). 6 | 7 | ### 🎭 Cenário: Sistema de E-Commerce 8 | 9 | Um sistema de e-commerce utiliza microsserviços (Pedido, Estoque e Pagamento) para gerenciar pedidos, estoque e pagamentos. O padrão Saga é implementado usando o padrão Outbox para comunicação consistente. 10 | 11 | * **📦 Serviço de Pedido:** 12 | - Recebe e processa pedidos de clientes. 13 | - Cria registros de pedido no banco de dados após a recepção do pedido. 14 | 15 | * **📦 Serviço de Estoque:** 16 | - Gerencia o estoque de produtos. 17 | - Recebe uma mensagem de pedido para reservar produtos em estoque. 18 | - Confirma a reserva, atualiza o banco de dados e registra uma mensagem na tabela Outbox. 19 | 20 | * **💳 Serviço de Pagamento:** 21 | - Processa pagamentos de pedidos. 22 | - Recebe uma mensagem de pedido para autorização de pagamento. 23 | - Valida o pagamento, autoriza-o, atualiza o banco de dados e registra uma mensagem na tabela Outbox. 24 | 25 | ### ⚙️ Fluxo de Execução: 26 | 27 | 1. O cliente faz um pedido através do serviço de Pedido. 28 | 2. O serviço de Pedido cria um registro na tabela Outbox com os detalhes do pedido. 29 | 3. A mensagem é enviada para o serviço de Estoque para reservar produtos. 30 | 4. O serviço de Estoque confirma a reserva, atualiza seu banco de dados e registra uma mensagem na tabela Outbox. 31 | 5. A mensagem é enviada para o serviço de Pagamento para autorização de pagamento. 32 | 6. O serviço de Pagamento valida o pagamento, autoriza-o, atualiza seu banco de dados e registra uma mensagem na tabela Outbox. 33 | 7. O serviço de Pedido verifica periodicamente a tabela Outbox para processar mensagens pendentes. 34 | 8. Se bem-sucedido, o pedido é marcado como confirmado e o cliente é notificado. 35 | 36 | ![Fluxo](docs/flow.png) 37 | 38 | ### 🏗️ Infraestrutura 39 | 40 | Este repositório fornece arquivos de configuração para implantar três serviços Django (Pedido, Estoque, Pagamento) no Kubernetes e Docker Compose. Cada serviço possui seu banco de dados PostgreSQL, e o RabbitMQ facilita a comunicação. Kong atua como gateway de API e camada de gerenciamento de microsserviços. 41 | 42 | ![Arquitetura](docs/architecture.png) 43 | 44 | ### 🛠️ Tecnologias Utilizadas 45 | 46 | 1. **Django:** Um framework web para desenvolvimento rápido de aplicativos Python. 47 | 2. **PostgreSQL:** Um robusto sistema de gerenciamento de banco de dados relacional. 48 | 3. **RabbitMQ:** Suporta comunicação assíncrona entre serviços. 49 | 4. **Kubernetes:** Orquestração de contêineres para automação de implantação e escalabilidade. 50 | 5. **Docker Compose:** Simplifica o gerenciamento de aplicativos Docker com vários contêineres. 51 | 6. **Kong:** Um gateway de API e camada de gerenciamento de microsserviços. 52 | 53 | ## 🚀 Instruções de Uso com Docker 54 | 55 | ### 🏁 Iniciando o Projeto 56 | 57 | 1. Navegue até o diretório [docker](docker). 58 | ```bash 59 | cd docker 60 | ``` 61 | 62 | 2. Execute o script de início: 63 | 64 | ```bash 65 | ./scripts/start.sh 66 | ``` 67 | 68 | 3. Acesse os serviços via: 69 | - Administração de Pedido: [http://localhost:8000/admin](http://localhost:8000/admin) 70 | - Administração de Estoque: [http://localhost:8001/admin](http://localhost:8001/admin) 71 | - Administração de Pagamento: [http://localhost:8002/admin](http://localhost:8002/admin) 72 | - API: [http://localhost:8080](http://localhost:8080) 73 | - Administração do Kong: [http://localhost:8082](http://localhost:8082) 74 | - Interface de Gerenciamento do RabbitMQ: [http://localhost:15672](http://localhost:15672) 75 | 76 | 4. Use estas credenciais: 77 | - Administração do Django: admin/admin 78 | - RabbitMQ: guest/guest 79 | 80 | ### 🛑 Parando o Projeto 81 | 82 | 1. Navegue até o diretório [docker](docker). 83 | ```bash 84 | cd docker 85 | ``` 86 | 87 | 2. Execute o script de parada: 88 | 89 | ```bash 90 | ./scripts/stop.sh 91 | ``` 92 | 93 | ## 🚀 Instruções de Uso com Kubernetes 94 | 95 | Este guia irá orientá-lo na configuração de um cluster Kubernetes usando k3d. Certifique-se de ter o Docker instalado no seu sistema antes de prosseguir. 96 | 97 | ### Configuração do Cluster Kubernetes 98 | 99 | Para configurar o cluster Kubernetes, siga estas etapas: 100 | 101 | 1. Navegue até o diretório [k8s](k8s). 102 | ```bash 103 | cd k8s 104 | ``` 105 | 2. Execute o script `setup.sh`. 106 | ```bash 107 | ./setup.sh 108 | ``` 109 | 110 | Este script automaticamente: 111 | 112 | 🚀 Instala k3d, kubectl, Krew e Helm se ainda não estiverem instalados. 113 | 114 | 🌟 Cria um cluster k3d chamado "saga" com mapeamento de portas para balanceamento de carga. 115 | 116 | Após executar o script, seu cluster Kubernetes estará configurado e pronto para uso. 117 | 118 | ### Instalando o Kong Ingress Controller 119 | 120 | 1. **Instale os CRDs do Gateway API antes de instalar o Kong Ingress Controller.** 121 | 122 | ```bash 123 | kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml 124 | ``` 125 | 126 | 2. **Crie uma instância de Gateway e GatewayClass para usar.** 127 | 128 | ```bash 129 | kubectl apply -f kong/kong-gateway.yaml 130 | ``` 131 | 132 | 3. **Instalação do Helm Chart** 133 | 134 | 1. Adicione os charts do Helm do Kong: 135 | ```bash 136 | helm repo add kong https://charts.konghq.com 137 | ``` 138 | 2. Atualize o repositório: 139 | ```bash 140 | helm repo update 141 | ``` 142 | 3. Instale o Controlador de Ingress do Kong e o Gateway do Kong com Helm: 143 | ```bash 144 | helm install kong kong/ingress -n kong --create-namespace --values kong/values.yaml 145 | ``` 146 | 147 | 4. **Verificar Instalação** 148 | 149 | Após a instalação, verifique se os pods do Controlador de Ingress do Kong estão em execução: 150 | 151 | ```bash 152 | curl -i 'localhost:8080' 153 | ``` 154 | 155 | Os resultados devem se parecer com isso: 156 | ```bash 157 | HTTP/1.1 404 Not Found 158 | Date: Sun, 28 Jan 2024 19:14:45 GMT 159 | Content-Type: application/json; charset=utf-8 160 | Connection: keep-alive 161 | Content-Length: 103 162 | X-Kong-Response-Latency: 0 163 | Server: kong/3.5.0 164 | X-Kong-Request-Id: fa55be13bee8575984a67514efbe224c 165 | 166 | { 167 | "message":"no Route matched with those values", 168 | "request_id":"fa55be13bee8575984a67514efbe224c" 169 | } 170 | ``` 171 | **Observação:** 172 | > Se encontrar `curl: (52) Empty reply from server`, aguarde um momento e tente novamente. 173 | 174 | 175 | 5. **Crie um Operador de Kubernetes RabbitMQ Cluster.** 176 | 177 | 1. Instale o Operador de Cluster RabbitMQ: 178 | ```bash 179 | kubectl rabbitmq install-cluster-operator 180 | ``` 181 | 2. Crie um cluster RabbitMQ: 182 | ```bash 183 | kubectl apply -f rabbitmq/rabbitmq.yaml 184 | ``` 185 | 3. Crie uma exchange saga: 186 | ```bash 187 | kubectl exec svc/rabbitmq -c rabbitmq -- rabbitmqadmin declare exchange name=saga type=topic -u guest -p guest 188 | ``` 189 | Os resultados devem ser semelhantes a isto: 190 | ```bash 191 | exchange declared 192 | ``` 193 | **Observação:** 194 | > O cluster RabbitMQ deve estar em execução 195 | 4. Acesse a Interface de Gerenciamento (opcional): 196 | ```bash 197 | kubectl rabbitmq manage rabbitmq 198 | ``` 199 | 200 | ### Instalando os serviços de pedido, estoque e pagamento usando Helm 📊 201 | 202 | Após configurar o cluster Kubernetes e instalar o Controlador de Ingress do Kong: 203 | 204 | 1. Use o Helm para criar os releases "order", "stock" e "payment" usando o chart Saga e os valores correspondentes: 205 | 206 | ```bash 207 | helm install order ./saga --values services/order/values.yaml 208 | helm install stock ./saga --values services/stock/values.yaml 209 | helm install payment ./saga --values services/payment/values.yaml 210 | ``` 211 | 212 | Isso cria três releases do Helm, "order", "stock" e "payment", com as configurações especificadas em seus respectivos arquivos `values.yaml`. 213 | 214 | Por favor, note que cada comando cria um release do Helm específico com suas próprias configurações. 215 | 216 | ### 🛑 Parando o Projeto 217 | 218 | 1. Execute o comando de exclusão do cluster: 219 | 220 | ```bash 221 | k3d cluster delete saga 222 | ``` 223 | 224 | ## Web App 225 | 226 | ![Web](docs/microservice_action.gif) 227 | 228 | ## 🧪 Testando Cenários com a Coleção do Postman 229 | 230 | 1. Instale o [Postman](https://www.postman.com/downloads/). 231 | 232 | 2. Importe a [coleção](docs/saga.postman_collection.json) do Postman. 233 | 234 | 3. A coleção contém cenários: 235 | - **Estoque Não Reservado:** Criar pedido com quantidade > 10. 236 | - **Pagamento Negado:** Criar pedido com valor > $1000. 237 | 238 | 4. Execute as solicitações para observar o comportamento do sistema. -------------------------------------------------------------------------------- /docker/compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | # Kong Services 5 | kong: 6 | profiles: [ kong ] 7 | extends: 8 | file: kong/compose.yaml 9 | service: kong 10 | 11 | # Kong Migrations Service 12 | kong-migrations: 13 | profiles: [ kong-db ] 14 | extends: 15 | file: kong/compose.yaml 16 | service: kong-migrations 17 | depends_on: 18 | kong-db: 19 | condition: service_healthy 20 | 21 | # Kong Migrations Up Service 22 | kong-migrations-up: 23 | profiles: [ kong-db ] 24 | extends: 25 | file: kong/compose.yaml 26 | service: kong-migrations-up 27 | depends_on: 28 | kong-db: 29 | condition: service_healthy 30 | 31 | # Kong Database Service 32 | kong-db: 33 | profiles: [ kong-db ] 34 | extends: 35 | file: kong/compose.yaml 36 | service: kong-db 37 | 38 | # Order Services 39 | order: 40 | profiles: [ order ] 41 | extends: 42 | file: services/order/compose.yaml 43 | service: order-service 44 | networks: 45 | - kong-net 46 | 47 | # Order Publish Service 48 | order-publish: 49 | profiles: [ order ] 50 | extends: 51 | file: services/order/compose.yaml 52 | service: order-publish 53 | depends_on: 54 | rabbitmq: 55 | condition: service_healthy 56 | 57 | # Order Subscribe Service 58 | order-subscribe: 59 | profiles: [ order ] 60 | extends: 61 | file: services/order/compose.yaml 62 | service: order-subscribe 63 | depends_on: 64 | rabbitmq: 65 | condition: service_healthy 66 | 67 | # Order Database Service 68 | order-db: 69 | profiles: [ order ] 70 | extends: 71 | file: services/order/compose.yaml 72 | service: order-db 73 | 74 | # Stock Services 75 | stock: 76 | profiles: [ stock ] 77 | extends: 78 | file: services/stock/compose.yaml 79 | service: stock-service 80 | networks: 81 | - kong-net 82 | 83 | # Stock Publish Service 84 | stock-publish: 85 | profiles: [ stock ] 86 | extends: 87 | file: services/stock/compose.yaml 88 | service: stock-publish 89 | depends_on: 90 | rabbitmq: 91 | condition: service_healthy 92 | 93 | # Stock Subscribe Service 94 | stock-subscribe: 95 | profiles: [ stock ] 96 | extends: 97 | file: services/stock/compose.yaml 98 | service: stock-subscribe 99 | depends_on: 100 | rabbitmq: 101 | condition: service_healthy 102 | 103 | # Stock Database Service 104 | stock-db: 105 | profiles: [ stock ] 106 | extends: 107 | file: services/stock/compose.yaml 108 | service: stock-db 109 | 110 | # Payment Services 111 | payment: 112 | profiles: [ payment ] 113 | extends: 114 | file: services/payment/compose.yaml 115 | service: payment-service 116 | networks: 117 | - kong-net 118 | 119 | # Payment Publish Service 120 | payment-publish: 121 | profiles: [ payment ] 122 | extends: 123 | file: services/payment/compose.yaml 124 | service: payment-publish 125 | depends_on: 126 | rabbitmq: 127 | condition: service_healthy 128 | 129 | # Payment Subscribe Service 130 | payment-subscribe: 131 | profiles: [ payment ] 132 | extends: 133 | file: services/payment/compose.yaml 134 | service: payment-subscribe 135 | depends_on: 136 | rabbitmq: 137 | condition: service_healthy 138 | 139 | # Payment Database Service 140 | payment-db: 141 | profiles: [ payment ] 142 | extends: 143 | file: services/payment/compose.yaml 144 | service: payment-db 145 | 146 | # RabbitMQ Service 147 | rabbitmq: 148 | profiles: [ rabbitmq ] 149 | image: rabbitmq:3.9.29-management 150 | volumes: 151 | - ./rabbitmq:/etc/rabbitmq/ 152 | healthcheck: 153 | test: [ "CMD-SHELL","rabbitmq-diagnostics -q ping" ] 154 | interval: 5s 155 | timeout: 5s 156 | retries: 5 157 | ports: 158 | - "15672:15672" 159 | - "5672:5672" 160 | - "61613:61613" 161 | networks: 162 | - saga 163 | 164 | networks: 165 | # Network for Kong Services 166 | kong-net: 167 | external: false 168 | 169 | # Network for RabbitMQ 170 | saga: 171 | driver: bridge 172 | 173 | volumes: 174 | # Volumes for Data Storage 175 | order-db-data: 176 | stock-db-data: 177 | payment-db-data: 178 | 179 | # Volumes for Kong 180 | kong_data: {} 181 | kong_prefix_vol: 182 | driver_opts: 183 | type: tmpfs 184 | device: tmpfs 185 | kong_tmp_vol: 186 | driver_opts: 187 | type: tmpfs 188 | device: tmpfs 189 | 190 | secrets: 191 | # Secret for Kong PostgreSQL Password 192 | kong_postgres_password: 193 | file: kong/POSTGRES_PASSWORD 194 | -------------------------------------------------------------------------------- /docker/kong/POSTGRES_PASSWORD: -------------------------------------------------------------------------------- 1 | kong -------------------------------------------------------------------------------- /docker/kong/compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | x-kong-config: 4 | &kong-env 5 | KONG_DATABASE: ${KONG_DATABASE:-off} 6 | KONG_PG_DATABASE: ${KONG_PG_DATABASE:-kong} 7 | KONG_PG_HOST: kong-db 8 | KONG_PG_USER: ${KONG_PG_USER:-kong} 9 | KONG_PG_PASSWORD_FILE: /run/secrets/kong_postgres_password 10 | 11 | volumes: 12 | kong_data: {} 13 | kong_prefix_vol: 14 | driver_opts: 15 | type: tmpfs 16 | device: tmpfs 17 | kong_tmp_vol: 18 | driver_opts: 19 | type: tmpfs 20 | device: tmpfs 21 | 22 | networks: 23 | kong-net: 24 | external: false 25 | 26 | services: 27 | kong-migrations: 28 | image: "${KONG_DOCKER_TAG:-kong:latest}" 29 | command: kong migrations bootstrap 30 | profiles: [ "database" ] 31 | depends_on: 32 | - kong-db 33 | environment: 34 | <<: *kong-env 35 | secrets: 36 | - kong_postgres_password 37 | networks: 38 | - kong-net 39 | restart: on-failure 40 | 41 | kong-migrations-up: 42 | image: "${KONG_DOCKER_TAG:-kong:latest}" 43 | command: kong migrations up && kong migrations finish 44 | profiles: [ "database" ] 45 | depends_on: 46 | - kong-db 47 | environment: 48 | <<: *kong-env 49 | secrets: 50 | - kong_postgres_password 51 | networks: 52 | - kong-net 53 | restart: on-failure 54 | 55 | kong: 56 | image: "${KONG_DOCKER_TAG:-kong:latest}" 57 | user: "${KONG_USER:-kong}" 58 | environment: 59 | <<: *kong-env 60 | KONG_ADMIN_ACCESS_LOG: /dev/stdout 61 | KONG_ADMIN_ERROR_LOG: /dev/stderr 62 | KONG_PROXY_LISTEN: "${KONG_PROXY_LISTEN:-0.0.0.0:8080}" 63 | KONG_ADMIN_LISTEN: "${KONG_ADMIN_LISTEN:-0.0.0.0:8081}" 64 | KONG_ADMIN_GUI_LISTEN: "${KONG_ADMIN_GUI_LISTEN:-0.0.0.0:8082}" 65 | KONG_PROXY_ACCESS_LOG: /dev/stdout 66 | KONG_PROXY_ERROR_LOG: /dev/stderr 67 | KONG_PREFIX: ${KONG_PREFIX:-/var/run/kong} 68 | KONG_DECLARATIVE_CONFIG: "/opt/kong/kong.yaml" 69 | secrets: 70 | - kong_postgres_password 71 | networks: 72 | - kong-net 73 | ports: 74 | # The following two environment variables default to an insecure value (0.0.0.0) 75 | # according to the CIS Security test. 76 | - "${KONG_INBOUND_PROXY_LISTEN:-0.0.0.0}:8080:8080/tcp" 77 | - "${KONG_INBOUND_SSL_PROXY_LISTEN:-0.0.0.0}:8443:8443/tcp" 78 | # Making them mandatory but undefined, like so would be backwards-breaking: 79 | # - "${KONG_INBOUND_PROXY_LISTEN?Missing inbound proxy host}:8000:8000/tcp" 80 | # - "${KONG_INBOUND_SSL_PROXY_LISTEN?Missing inbound proxy ssl host}:8443:8443/tcp" 81 | # Alternative is deactivating check 5.13 in the security bench, if we consider Kong's own config to be enough security here 82 | 83 | - "127.0.0.1:8081:8081/tcp" 84 | - "127.0.0.1:8444:8444/tcp" 85 | - "127.0.0.1:8082:8082/tcp" 86 | healthcheck: 87 | test: [ "CMD", "kong", "health" ] 88 | interval: 10s 89 | timeout: 10s 90 | retries: 10 91 | restart: on-failure 92 | read_only: true 93 | volumes: 94 | - kong_prefix_vol:${KONG_PREFIX:-/var/run/kong} 95 | - kong_tmp_vol:/tmp 96 | - ./config:/opt/kong 97 | security_opt: 98 | - no-new-privileges 99 | 100 | kong-db: 101 | image: postgres:9.5 102 | profiles: [ "database" ] 103 | environment: 104 | POSTGRES_DB: ${KONG_PG_DATABASE:-kong} 105 | POSTGRES_USER: ${KONG_PG_USER:-kong} 106 | POSTGRES_PASSWORD_FILE: /run/secrets/kong_postgres_password 107 | secrets: 108 | - kong_postgres_password 109 | healthcheck: 110 | test: 111 | [ 112 | "CMD", 113 | "pg_isready", 114 | "-d", 115 | "${KONG_PG_DATABASE:-kong}", 116 | "-U", 117 | "${KONG_PG_USER:-kong}" 118 | ] 119 | interval: 30s 120 | timeout: 30s 121 | retries: 3 122 | restart: on-failure 123 | stdin_open: true 124 | tty: true 125 | networks: 126 | - kong-net 127 | volumes: 128 | - kong_data:/var/lib/postgresql/data 129 | 130 | secrets: 131 | kong_postgres_password: 132 | file: POSTGRES_PASSWORD -------------------------------------------------------------------------------- /docker/kong/config/kong.yaml: -------------------------------------------------------------------------------- 1 | # a very minimal declarative config file 2 | _format_version: "2.1" 3 | _transform: true 4 | 5 | services: 6 | - name: order 7 | url: http://order:8000 8 | routes: 9 | - name: order 10 | paths: 11 | - /order 12 | - name: stock 13 | url: http://stock:8001 14 | routes: 15 | - name: stock 16 | paths: 17 | - /stock 18 | - name: payment 19 | url: http://payment:8002 20 | routes: 21 | - name: payment 22 | paths: 23 | - /payment 24 | plugins: 25 | - name: cors 26 | config: 27 | origins: 28 | - "*" 29 | methods: 30 | - GET 31 | - POST 32 | headers: 33 | - Accept 34 | - Accept-Version 35 | - Content-Length 36 | - Content-MD5 37 | - Content-Type 38 | - Date 39 | - X-Auth-Token 40 | exposed_headers: 41 | - X-Auth-Token 42 | credentials: true 43 | max_age: 3600 44 | -------------------------------------------------------------------------------- /docker/rabbitmq/enabled_plugins: -------------------------------------------------------------------------------- 1 | [rabbitmq_management,rabbitmq_stomp,rabbitmq_shovel,rabbitmq_shovel_management]. 2 | -------------------------------------------------------------------------------- /docker/rabbitmq/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | loopback_users.guest = false 2 | -------------------------------------------------------------------------------- /docker/scripts/manage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script Name: manage.sh 4 | # Description: Run Django management commands in a Docker container. 5 | # 6 | # Usage: 7 | # ./manage.sh [additional_arguments] 8 | # 9 | # Parameters: 10 | # - : Name of the Docker service/container where Django is running. 11 | # - : Django management command to be executed. 12 | # - [additional_arguments]: Additional arguments to be passed to the Django management command. 13 | # 14 | # Examples: 15 | # ./manage.sh web migrate # Run Django's migrate command in the 'web' service. 16 | # ./manage.sh worker process_tasks --all # Run a custom Django management command in the 'worker' service with additional arguments. 17 | # 18 | # Note: 19 | # - This script assumes that Docker Compose is installed on the system. 20 | # - Ensure proper permissions to execute this script. 21 | # - Make sure to specify the correct service name and Django management command. 22 | # - Additional arguments are optional and can be used to pass arguments to the Django management command. 23 | 24 | # Extract arguments 25 | SERVICE_NAME=$1 26 | DJANGO_COMMAND=$2 27 | 28 | # Run the Django management command in the specified Docker service 29 | docker compose run --rm "$SERVICE_NAME" python manage.py "$DJANGO_COMMAND" "${@:3}" 30 | -------------------------------------------------------------------------------- /docker/scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script Name: run.sh 4 | # Description: A simple Bash script for managing Docker Compose commands with profiles. 5 | # 6 | # Usage: 7 | # ./run.sh 8 | # 9 | # Parameters: 10 | # - : Docker Compose command to execute. Supported commands: build, up, stop, down. 11 | # - : Docker Compose profiles to apply to the command. 12 | # 13 | # Examples: 14 | # ./run.sh build order # Build Docker images for the order profile. 15 | # ./run.sh up order stock payment # Start containers in the order, stock and payment profiles. 16 | # 17 | # Note: 18 | # - This script requires Docker Compose to be installed on the system. 19 | # - Ensure proper permissions are set to execute this script. 20 | 21 | 22 | # Check if the number of arguments is valid 23 | if [[ $# -lt 2 ]]; then 24 | echo "Usage: $0 " 25 | exit 1 26 | fi 27 | 28 | # Define the Docker Compose command 29 | COMPOSE_CMD="docker compose" 30 | 31 | # Define the command passed as an argument 32 | CMD="$1" 33 | 34 | # Define the profiles passed as arguments 35 | PROFILES="${@:2}" 36 | 37 | # Define the environment variable COMPOSE_PROFILES 38 | export COMPOSE_PROFILES="${PROFILES// /,}" 39 | 40 | # Check the passed command and execute the corresponding action 41 | case $CMD in 42 | "build") 43 | $COMPOSE_CMD build 44 | ;; 45 | "up") 46 | $COMPOSE_CMD up -d --remove-orphans 47 | ;; 48 | "stop") 49 | $COMPOSE_CMD stop 50 | ;; 51 | "down") 52 | $COMPOSE_CMD down -v --remove-orphans 53 | ;; 54 | *) 55 | echo "Invalid command. Available options: build, up, stop, down." 56 | exit 1 57 | ;; 58 | esac 59 | 60 | # Clear the COMPOSE_PROFILES environment variable 61 | unset COMPOSE_PROFILES 62 | -------------------------------------------------------------------------------- /docker/scripts/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | check_status() { 6 | status=$(curl -s -o /dev/null 2>&1 -w "%{http_code}" http://127.0.0.1:15672) 7 | 8 | if [ "$status" -eq 200 ]; then 9 | return 0 10 | else 11 | return 1 12 | fi 13 | } 14 | 15 | echo "Running migrations..." 16 | 17 | source scripts/manage.sh order migrate > /dev/null 2>&1 18 | source scripts/manage.sh stock migrate > /dev/null 2>&1 19 | source scripts/manage.sh payment migrate > /dev/null 2>&1 20 | source scripts/run.sh up kong-db > /dev/null 2>&1 21 | 22 | echo "Loading data..." 23 | 24 | source scripts/manage.sh order loaddata order > /dev/null 2>&1 25 | source scripts/manage.sh stock loaddata stock > /dev/null 2>&1 26 | source scripts/manage.sh payment loaddata payment > /dev/null 2>&1 27 | 28 | # Starting RabbitMQ to create the exchange 29 | docker compose up -d rabbitmq > /dev/null 2>&1 30 | 31 | while ! check_status; do 32 | echo "Creating saga exchange..." 33 | sleep 10 34 | done 35 | 36 | docker compose exec rabbitmq rabbitmqadmin declare exchange name=saga type=topic > /dev/null 2>&1 37 | 38 | # Stopping RabbitMQ 39 | docker compose stop rabbitmq > /dev/null 2>&1 40 | 41 | echo "Starting services..." 42 | 43 | source scripts/run.sh up order stock payment rabbitmq kong> /dev/null 2>&1 -------------------------------------------------------------------------------- /docker/scripts/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Stopping services..." 6 | 7 | source scripts/run.sh down order stock payment rabbitmq kong kong-db > /dev/null 2>&1 8 | 9 | docker volume prune -f > /dev/null 2>&1 -------------------------------------------------------------------------------- /docker/services/order/.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.DS_Store 8 | **/__pycache__ 9 | **/.venv 10 | **/.classpath 11 | **/.dockerignore 12 | **/.env 13 | **/.git 14 | **/.gitignore 15 | **/.project 16 | **/.settings 17 | **/.toolstarget 18 | **/.vs 19 | **/.vscode 20 | **/*.*proj.user 21 | **/*.dbmdl 22 | **/*.jfm 23 | **/bin 24 | **/charts 25 | **/docker-compose* 26 | **/compose* 27 | **/Dockerfile* 28 | **/node_modules 29 | **/npm-debug.log 30 | **/obj 31 | **/secrets.dev.yaml 32 | **/values.dev.yaml 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /docker/services/order/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = false 9 | max_line_length = 120 10 | tab_width = 4 11 | 12 | [{*.yaml,*.yml}] 13 | indent_size = 2 -------------------------------------------------------------------------------- /docker/services/order/.env.example: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY='secret_key' 2 | DJANGO_DEBUG=True 3 | DJANGO_ALLOWED_HOSTS=* 4 | DATABASE_URL=postgres://user:password@order-db:5432/db 5 | DJANGO_LANGUAGE_CODE=en-us 6 | DJANGO_TIME_ZONE=UTC 7 | DJANGO_USE_I18N=True 8 | DJANGO_USE_TZ=True 9 | DJANGO_CONFIGURATION=Dev 10 | POSTGRES_DB=db 11 | POSTGRES_USER=user 12 | POSTGRES_PASSWORD=password -------------------------------------------------------------------------------- /docker/services/order/.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | 4 | # Ruff 5 | .ruff_cache 6 | 7 | # Envs 8 | .env 9 | .venv 10 | 11 | # Staticfiles 12 | staticfiles 13 | 14 | # Logs 15 | *.log -------------------------------------------------------------------------------- /docker/services/order/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 'v0.1.14' 4 | hooks: 5 | - id: ruff 6 | args: [ --fix ] 7 | - id: ruff-format 8 | - repo: https://github.com/python-poetry/poetry 9 | rev: '1.7.0' 10 | hooks: 11 | - id: poetry-check 12 | - id: poetry-lock 13 | - id: poetry-export 14 | args: ["-f", "requirements.txt", "-o", "requirements.txt"] 15 | - id: poetry-install -------------------------------------------------------------------------------- /docker/services/order/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Comments are provided throughout this file to help you get started. 4 | # If you need more help, visit the Dockerfile reference guide at 5 | # https://docs.docker.com/engine/reference/builder/ 6 | 7 | ARG PYTHON_VERSION=3.12.1 8 | FROM python:${PYTHON_VERSION}-alpine as base 9 | 10 | # Prevents Python from writing pyc files. 11 | ENV PYTHONDONTWRITEBYTECODE=1 12 | 13 | # Keeps Python from buffering stdout and stderr to avoid situations where 14 | # the application crashes without emitting any logs due to buffering. 15 | ENV PYTHONUNBUFFERED=1 16 | 17 | # Required for the djnago-configuration library 18 | ENV DJANGO_SETTINGS_MODULE "src.settings" 19 | 20 | WORKDIR /app 21 | 22 | # Create a non-privileged user that the app will run under. 23 | # See https://docs.docker.com/go/dockerfile-user-best-practices/ 24 | ARG UID=10001 25 | RUN adduser \ 26 | --disabled-password \ 27 | --gecos "" \ 28 | --home "/nonexistent" \ 29 | --shell "/sbin/nologin" \ 30 | --no-create-home \ 31 | --uid "${UID}" \ 32 | django 33 | 34 | # Download dependencies as a separate step to take advantage of Docker's caching. 35 | # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. 36 | # Leverage a bind mount to requirements.txt to avoid having to copy them into 37 | # into this layer. 38 | RUN --mount=type=cache,target=/root/.cache/pip \ 39 | --mount=type=bind,source=requirements.txt,target=requirements.txt \ 40 | python -m pip install -r requirements.txt 41 | 42 | # Creating folder for gunicorn logs. 43 | RUN mkdir /var/log/gunicorn 44 | 45 | # Granting log permissions to non-privileged user. 46 | RUN chown -R django:django /var/log/gunicorn 47 | 48 | # Switch to the non-privileged user to run the application. 49 | USER django 50 | 51 | # Copy the source code into the container. 52 | COPY --chown=django:django . . 53 | 54 | # Expose the port that the application listens on. 55 | EXPOSE 8000 56 | 57 | # Run the application. 58 | CMD ["gunicorn", "-c", "/app/gunicorn.conf.py", "src.wsgi"] 59 | -------------------------------------------------------------------------------- /docker/services/order/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RAKT Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker/services/order/README.md: -------------------------------------------------------------------------------- 1 | # 👋 Hi, I'm Template -------------------------------------------------------------------------------- /docker/services/order/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/order/__init__.py -------------------------------------------------------------------------------- /docker/services/order/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | order-service: 3 | build: 4 | context: . 5 | ports: 6 | - 8000:8000 7 | volumes: 8 | - .:/app 9 | env_file: 10 | - .env 11 | networks: 12 | - saga 13 | depends_on: 14 | order-db: 15 | condition: service_healthy 16 | order-publish: 17 | build: 18 | context: . 19 | command: [ "python", "manage.py", "publish" ] 20 | volumes: 21 | - .:/app 22 | env_file: 23 | - .env 24 | networks: 25 | - saga 26 | depends_on: 27 | order-db: 28 | condition: service_healthy 29 | order-subscribe: 30 | build: 31 | context: . 32 | command: [ "python", "manage.py", "subscribe", "src.core.callback.callback", "/exchange/saga/order.v1", "order.v1" ] 33 | volumes: 34 | - .:/app 35 | env_file: 36 | - .env 37 | networks: 38 | - saga 39 | depends_on: 40 | order-db: 41 | condition: service_healthy 42 | order-db: 43 | image: postgres:16 44 | platform: linux/amd64 45 | restart: always 46 | volumes: 47 | - order-db-data:/var/lib/postgresql/data 48 | env_file: 49 | - .env 50 | expose: 51 | - 5432 52 | healthcheck: 53 | test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-db}" ] 54 | interval: 5s 55 | timeout: 5s 56 | retries: 5 57 | networks: 58 | - saga 59 | volumes: 60 | order-db-data: 61 | 62 | networks: 63 | saga: 64 | driver: bridge -------------------------------------------------------------------------------- /docker/services/order/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | # Bind to 0.0.0.0:8000 (accept connections from outside the container) 4 | bind = "0.0.0.0:8000" 5 | 6 | # Number of workers based on available CPUs in the container 7 | workers = multiprocessing.cpu_count() * 2 + 1 8 | 9 | # Worker class selection based on application type (asynchronous or not) 10 | worker_class = "gthread" # Or 'gevent' for asynchronous applications 11 | 12 | # Threads per worker for I/O intensive operations 13 | threads = 4 14 | 15 | # Timeout for requests 16 | timeout = 30 17 | 18 | # Worker temporary directory for better performance 19 | worker_tmp_dir = "/dev/shm" 20 | 21 | # Log for access and error 22 | logconfig_dict = { 23 | "version": 1, 24 | "formatters": { 25 | "json": { 26 | "()": "pythonjsonlogger.jsonlogger.JsonFormatter", 27 | "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d", 28 | }, 29 | }, 30 | "handlers": { 31 | "access": { 32 | "class": "logging.FileHandler", 33 | "filename": "/var/log/gunicorn/access.log", 34 | "formatter": "json", 35 | }, 36 | "error": { 37 | "class": "logging.FileHandler", 38 | "filename": "/var/log/gunicorn/error.log", 39 | "formatter": "json", 40 | }, 41 | }, 42 | "root": {"level": "INFO", "handlers": []}, 43 | "loggers": { 44 | "gunicorn.access": { 45 | "level": "INFO", 46 | "handlers": ["access"], 47 | "propagate": False, 48 | }, 49 | "gunicorn.error": { 50 | "level": "INFO", 51 | "handlers": ["error"], 52 | "propagate": False, 53 | }, 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /docker/services/order/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /docker/services/order/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "template" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Hugo Brilhante "] 6 | readme = "README.md" 7 | packages = [{include = "template", from = "src"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "3.12.1" 11 | django = "^5.0.1" 12 | django-configurations = {extras = ["cache", "database", "email", "search"], version = "^2.5"} 13 | whitenoise = "^6.6.0" 14 | gunicorn = "^21.2.0" 15 | psycopg2-binary = "^2.9.9" 16 | python-json-logger = "^2.0.7" 17 | djangorestframework = "^3.14.0" 18 | django-outbox-pattern = "^1.0.1" 19 | 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | pre-commit = "3.5.0" 23 | 24 | [tool.ruff.lint.isort] 25 | force-single-line = true 26 | 27 | [build-system] 28 | requires = ["poetry-core"] 29 | build-backend = "poetry.core.masonry.api" 30 | -------------------------------------------------------------------------------- /docker/services/order/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 ; python_full_version == "3.12.1" \ 2 | --hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \ 3 | --hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed 4 | dj-database-url==2.1.0 ; python_full_version == "3.12.1" \ 5 | --hash=sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0 \ 6 | --hash=sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f 7 | dj-email-url==1.0.6 ; python_full_version == "3.12.1" \ 8 | --hash=sha256:55ffe3329e48f54f8a75aa36ece08f365e09d61f8a209773ef09a1d4760e699a \ 9 | --hash=sha256:cbd08327fbb08b104eac160fb4703f375532e4c0243eb230f5b960daee7a96db 10 | dj-search-url==0.1 ; python_full_version == "3.12.1" \ 11 | --hash=sha256:424d1a5852500b3c118abfdd0e30b3e0016fe68e7ed27b8553a67afa20d4fb40 12 | django-cache-url==3.4.5 ; python_full_version == "3.12.1" \ 13 | --hash=sha256:5f350759978483ab85dc0e3e17b3d53eed3394a28148f6bf0f53d11d0feb5b3c \ 14 | --hash=sha256:eb9fb194717524348c95cad9905b70b647452741c1d9e481fac6d2125f0ad917 15 | django-configurations[cache,database,email,search]==2.5 ; python_full_version == "3.12.1" \ 16 | --hash=sha256:63fa252c40dc88ea17b8b90f5f4a31a2726e586acb1ff0edc74c228c61f19e5d \ 17 | --hash=sha256:cf063b99ad30013df49eaa971bd8543deffb008ff080cf3a92955dbccfe81a5c 18 | django-outbox-pattern==1.0.1 ; python_full_version == "3.12.1" \ 19 | --hash=sha256:9db96fe073c4258ff0f992b454a4815fa06709defd01008d67f7d74b7392cd06 \ 20 | --hash=sha256:ae04163a42d1e9d156a0a03a720f3e20795082df37277fa185db5b1fd95029c5 21 | django==5.0.1 ; python_full_version == "3.12.1" \ 22 | --hash=sha256:8c8659665bc6e3a44fefe1ab0a291e5a3fb3979f9a8230be29de975e57e8f854 \ 23 | --hash=sha256:f47a37a90b9bbe2c8ec360235192c7fddfdc832206fcf618bb849b39256affc1 24 | djangorestframework==3.14.0 ; python_full_version == "3.12.1" \ 25 | --hash=sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8 \ 26 | --hash=sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08 27 | docopt==0.6.2 ; python_full_version == "3.12.1" \ 28 | --hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491 29 | gunicorn==21.2.0 ; python_full_version == "3.12.1" \ 30 | --hash=sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0 \ 31 | --hash=sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033 32 | packaging==23.2 ; python_full_version == "3.12.1" \ 33 | --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ 34 | --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 35 | psycopg2-binary==2.9.9 ; python_full_version == "3.12.1" \ 36 | --hash=sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9 \ 37 | --hash=sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77 \ 38 | --hash=sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e \ 39 | --hash=sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84 \ 40 | --hash=sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3 \ 41 | --hash=sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2 \ 42 | --hash=sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67 \ 43 | --hash=sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876 \ 44 | --hash=sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152 \ 45 | --hash=sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f \ 46 | --hash=sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a \ 47 | --hash=sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6 \ 48 | --hash=sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503 \ 49 | --hash=sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f \ 50 | --hash=sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493 \ 51 | --hash=sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996 \ 52 | --hash=sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f \ 53 | --hash=sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e \ 54 | --hash=sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59 \ 55 | --hash=sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94 \ 56 | --hash=sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7 \ 57 | --hash=sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682 \ 58 | --hash=sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420 \ 59 | --hash=sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae \ 60 | --hash=sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291 \ 61 | --hash=sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe \ 62 | --hash=sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980 \ 63 | --hash=sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93 \ 64 | --hash=sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692 \ 65 | --hash=sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119 \ 66 | --hash=sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716 \ 67 | --hash=sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472 \ 68 | --hash=sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b \ 69 | --hash=sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2 \ 70 | --hash=sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc \ 71 | --hash=sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c \ 72 | --hash=sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5 \ 73 | --hash=sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab \ 74 | --hash=sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984 \ 75 | --hash=sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9 \ 76 | --hash=sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf \ 77 | --hash=sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0 \ 78 | --hash=sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f \ 79 | --hash=sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212 \ 80 | --hash=sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb \ 81 | --hash=sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be \ 82 | --hash=sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90 \ 83 | --hash=sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041 \ 84 | --hash=sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7 \ 85 | --hash=sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860 \ 86 | --hash=sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d \ 87 | --hash=sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245 \ 88 | --hash=sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27 \ 89 | --hash=sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417 \ 90 | --hash=sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359 \ 91 | --hash=sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202 \ 92 | --hash=sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0 \ 93 | --hash=sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7 \ 94 | --hash=sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba \ 95 | --hash=sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1 \ 96 | --hash=sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd \ 97 | --hash=sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07 \ 98 | --hash=sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98 \ 99 | --hash=sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55 \ 100 | --hash=sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d \ 101 | --hash=sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972 \ 102 | --hash=sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f \ 103 | --hash=sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e \ 104 | --hash=sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26 \ 105 | --hash=sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957 \ 106 | --hash=sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53 \ 107 | --hash=sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52 108 | python-json-logger==2.0.7 ; python_full_version == "3.12.1" \ 109 | --hash=sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c \ 110 | --hash=sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd 111 | pytz==2023.3.post1 ; python_full_version == "3.12.1" \ 112 | --hash=sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b \ 113 | --hash=sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7 114 | sqlparse==0.4.4 ; python_full_version == "3.12.1" \ 115 | --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ 116 | --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c 117 | stomp-py==8.1.0 ; python_full_version == "3.12.1" \ 118 | --hash=sha256:49bc4cf5a4b17b6dce6dc2da0cb869f6522f77eff5ba46ee3e26ceb4793be0df \ 119 | --hash=sha256:b4737d002684639753fbf12dedecb8aa85e5e260749c947acd49c482db42a594 120 | typing-extensions==4.9.0 ; python_full_version == "3.12.1" \ 121 | --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ 122 | --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd 123 | tzdata==2023.4 ; sys_platform == "win32" and python_full_version == "3.12.1" \ 124 | --hash=sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3 \ 125 | --hash=sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9 126 | websocket-client==1.7.0 ; python_full_version == "3.12.1" \ 127 | --hash=sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6 \ 128 | --hash=sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588 129 | whitenoise==6.6.0 ; python_full_version == "3.12.1" \ 130 | --hash=sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251 \ 131 | --hash=sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146 132 | -------------------------------------------------------------------------------- /docker/services/order/src/__init__.py: -------------------------------------------------------------------------------- 1 | from configurations import importer 2 | 3 | importer.install(check_options=True) 4 | -------------------------------------------------------------------------------- /docker/services/order/src/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for src project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 16 | 17 | application = get_asgi_application() 18 | -------------------------------------------------------------------------------- /docker/services/order/src/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/order/src/core/__init__.py -------------------------------------------------------------------------------- /docker/services/order/src/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Order 3 | 4 | admin.site.register(Order) 5 | -------------------------------------------------------------------------------- /docker/services/order/src/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "src.core" 7 | -------------------------------------------------------------------------------- /docker/services/order/src/core/callback.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | from django_outbox_pattern.payloads import Payload 3 | 4 | from .models import Order 5 | 6 | 7 | def callback(payload: Payload): 8 | order_id = payload.body["order_id"] 9 | status = payload.body["status"] 10 | with transaction.atomic(): 11 | order = Order.objects.get(order_id=order_id) 12 | if not order.status == status: 13 | order.status = status 14 | order.save() 15 | payload.save() 16 | -------------------------------------------------------------------------------- /docker/services/order/src/core/fixtures/order.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$600000$HDxNWYr1aaj3uizUw73YAu$BflQx1Hl0BmCTeDawhjVBuqDbaIM4VJOCSfMKB+G17g=", 7 | "last_login": "2024-01-18T17:31:49.328Z", 8 | "is_superuser": true, 9 | "username": "admin", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "admin@admin.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2024-01-18T17:30:18.484Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /docker/services/order/src/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-01-19 01:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Order", 14 | fields=[ 15 | ("order_id", models.AutoField(primary_key=True, serialize=False)), 16 | ("customer_id", models.CharField(max_length=20)), 17 | ("product_id", models.CharField(max_length=20)), 18 | ("quantity", models.PositiveIntegerField(default=0)), 19 | ("amount", models.CharField(max_length=20)), 20 | ( 21 | "status", 22 | models.CharField( 23 | choices=[ 24 | ("created", "Created"), 25 | ("reserved", "Reserved"), 26 | ("not_reserved", "Not Reserved"), 27 | ("payment_confirmed", "Payment Confirmed"), 28 | ("payment_denied", "Payment Denied"), 29 | ], 30 | default="created", 31 | max_length=20, 32 | ), 33 | ), 34 | ("created", models.DateTimeField(auto_now_add=True)), 35 | ], 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /docker/services/order/src/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/order/src/core/migrations/__init__.py -------------------------------------------------------------------------------- /docker/services/order/src/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_outbox_pattern.decorators import Config 3 | from django_outbox_pattern.decorators import publish 4 | 5 | 6 | @publish( 7 | [ 8 | Config( 9 | destination="/exchange/saga/stock", 10 | version="v1", 11 | serializer="order_serializer", 12 | ), 13 | Config( 14 | destination="/exchange/saga/payment", 15 | version="v1", 16 | serializer="order_serializer", 17 | ), 18 | ] 19 | ) 20 | class Order(models.Model): 21 | STATUS_CHOICES = ( 22 | ("created", "Created"), 23 | ("reserved", "Reserved"), 24 | ("not_reserved", "Not Reserved"), 25 | ("payment_confirmed", "Payment Confirmed"), 26 | ("payment_denied", "Payment Denied"), 27 | ) 28 | 29 | order_id = models.AutoField(primary_key=True) 30 | customer_id = models.CharField(max_length=20) 31 | product_id = models.CharField(max_length=20) 32 | quantity = models.PositiveIntegerField(default=0) 33 | amount = models.CharField(max_length=20) 34 | status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="created") 35 | created = models.DateTimeField(auto_now_add=True) 36 | 37 | def order_serializer(self): 38 | return { 39 | "amount": self.amount, 40 | "customer_id": self.customer_id, 41 | "order_id": self.order_id, 42 | "product_id": self.product_id, 43 | "quantity": self.quantity, 44 | "status": self.status, 45 | } 46 | 47 | def __str__(self): 48 | return f"Order {self.order_id} - {self.status}" 49 | -------------------------------------------------------------------------------- /docker/services/order/src/core/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Order 3 | 4 | 5 | class OrderSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Order 8 | fields = [ 9 | "order_id", 10 | "customer_id", 11 | "product_id", 12 | "amount", 13 | "quantity", 14 | "status", 15 | ] 16 | -------------------------------------------------------------------------------- /docker/services/order/src/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import OrderListCreateView 4 | 5 | 6 | urlpatterns = [ 7 | path("api/v1/orders/", OrderListCreateView.as_view(), name="order-list-create"), 8 | ] 9 | -------------------------------------------------------------------------------- /docker/services/order/src/core/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Order 3 | from .serializers import OrderSerializer 4 | 5 | 6 | class OrderListCreateView(generics.ListCreateAPIView): 7 | queryset = Order.objects.all() 8 | serializer_class = OrderSerializer 9 | -------------------------------------------------------------------------------- /docker/services/order/src/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for src project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | import os 13 | import sys 14 | 15 | from pathlib import Path 16 | 17 | from configurations import Configuration, values 18 | 19 | 20 | class Base(Configuration): 21 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 22 | BASE_DIR = Path(__file__).resolve().parent.parent 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = values.Value() 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = values.BooleanValue(False) 32 | 33 | ALLOWED_HOSTS = values.ListValue([]) 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | "django.contrib.admin", 39 | "django.contrib.auth", 40 | "django.contrib.contenttypes", 41 | "django.contrib.sessions", 42 | "django.contrib.messages", 43 | "django.contrib.staticfiles", 44 | # Installed Apps 45 | "rest_framework", 46 | "django_outbox_pattern", 47 | # App locals 48 | "src.core.apps.CoreConfig", 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | "django.middleware.security.SecurityMiddleware", 53 | "whitenoise.middleware.WhiteNoiseMiddleware", 54 | "django.contrib.sessions.middleware.SessionMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.messages.middleware.MessageMiddleware", 59 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 60 | ] 61 | 62 | ROOT_URLCONF = "src.urls" 63 | 64 | TEMPLATES = [ 65 | { 66 | "BACKEND": "django.template.backends.django.DjangoTemplates", 67 | "DIRS": [], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = "src.wsgi.application" 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 84 | 85 | DATABASES = values.DatabaseURLValue( 86 | "sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3") 87 | ) 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 104 | }, 105 | ] 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 109 | 110 | LANGUAGE_CODE = values.Value("en-us") 111 | 112 | TIME_ZONE = values.Value("UTC") 113 | 114 | USE_I18N = values.BooleanValue(True) 115 | 116 | USE_TZ = values.BooleanValue(True) 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 120 | 121 | STATIC_URL = "/static/" 122 | 123 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 124 | 125 | STORAGES = { 126 | "staticfiles": { 127 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 128 | }, 129 | } 130 | 131 | # See https://docs.djangoproject.com/en/4.2/topics/email/#console-backend 132 | EMAIL = values.EmailURLValue("console://") 133 | 134 | # Default primary key field type 135 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 136 | 137 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 138 | 139 | DJANGO_OUTBOX_PATTERN = { 140 | "DEFAULT_STOMP_HOST_AND_PORTS": [("rabbitmq", 61613)], 141 | } 142 | 143 | 144 | class Dev(Base): 145 | # See https://docs.djangoproject.com/en/4.2/topics/cache/#dummy-caching-for-development 146 | CACHES = values.CacheURLValue("dummy://") 147 | # See http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development 148 | Base.INSTALLED_APPS.insert(0, "whitenoise.runserver_nostatic") 149 | 150 | 151 | class Prod(Base): 152 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/#csrf-cookie-secure 153 | CSRF_COOKIE_SECURE = True 154 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/#session-cookie-secure 155 | SESSION_COOKIE_SECURE = True 156 | 157 | LOGGING = { 158 | "version": 1, 159 | "disable_existing_loggers": False, 160 | "formatters": { 161 | "json": { 162 | "()": "pythonjsonlogger.jsonlogger.JsonFormatter", 163 | "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d", 164 | }, 165 | }, 166 | "handlers": { 167 | "stdout": { 168 | "level": "INFO", 169 | "class": "logging.StreamHandler", 170 | "stream": sys.stdout, 171 | "formatter": "json", 172 | }, 173 | }, 174 | "loggers": { 175 | "": { 176 | "handlers": ["stdout"], 177 | "level": "INFO", 178 | "propagate": True, 179 | }, 180 | }, 181 | } 182 | -------------------------------------------------------------------------------- /docker/services/order/src/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [path("admin/", admin.site.urls), path("", include("src.core.urls"))] 5 | -------------------------------------------------------------------------------- /docker/services/order/src/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for src project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /docker/services/order/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/order/tests/__init__.py -------------------------------------------------------------------------------- /docker/services/payment/.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.DS_Store 8 | **/__pycache__ 9 | **/.venv 10 | **/.classpath 11 | **/.dockerignore 12 | **/.env 13 | **/.git 14 | **/.gitignore 15 | **/.project 16 | **/.settings 17 | **/.toolstarget 18 | **/.vs 19 | **/.vscode 20 | **/*.*proj.user 21 | **/*.dbmdl 22 | **/*.jfm 23 | **/bin 24 | **/charts 25 | **/docker-compose* 26 | **/compose* 27 | **/Dockerfile* 28 | **/node_modules 29 | **/npm-debug.log 30 | **/obj 31 | **/secrets.dev.yaml 32 | **/values.dev.yaml 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /docker/services/payment/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = false 9 | max_line_length = 120 10 | tab_width = 4 11 | 12 | [{*.yaml,*.yml}] 13 | indent_size = 2 -------------------------------------------------------------------------------- /docker/services/payment/.env.example: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY='secret_key' 2 | DJANGO_DEBUG=True 3 | DJANGO_ALLOWED_HOSTS=* 4 | DATABASE_URL=postgres://user:password@payment-db:5432/db 5 | DJANGO_LANGUAGE_CODE=en-us 6 | DJANGO_TIME_ZONE=UTC 7 | DJANGO_USE_I18N=True 8 | DJANGO_USE_TZ=True 9 | DJANGO_CONFIGURATION=Dev 10 | POSTGRES_DB=db 11 | POSTGRES_USER=user 12 | POSTGRES_PASSWORD=password -------------------------------------------------------------------------------- /docker/services/payment/.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | 4 | # Ruff 5 | .ruff_cache 6 | 7 | # Envs 8 | .env 9 | .venv 10 | 11 | # Staticfiles 12 | staticfiles 13 | 14 | # Logs 15 | *.log -------------------------------------------------------------------------------- /docker/services/payment/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 'v0.1.14' 4 | hooks: 5 | - id: ruff 6 | args: [ --fix ] 7 | - id: ruff-format 8 | - repo: https://github.com/python-poetry/poetry 9 | rev: '1.7.0' 10 | hooks: 11 | - id: poetry-check 12 | - id: poetry-lock 13 | - id: poetry-export 14 | args: ["-f", "requirements.txt", "-o", "requirements.txt"] 15 | - id: poetry-install -------------------------------------------------------------------------------- /docker/services/payment/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Comments are provided throughout this file to help you get started. 4 | # If you need more help, visit the Dockerfile reference guide at 5 | # https://docs.docker.com/engine/reference/builder/ 6 | 7 | ARG PYTHON_VERSION=3.12.1 8 | FROM python:${PYTHON_VERSION}-alpine as base 9 | 10 | # Prevents Python from writing pyc files. 11 | ENV PYTHONDONTWRITEBYTECODE=1 12 | 13 | # Keeps Python from buffering stdout and stderr to avoid situations where 14 | # the application crashes without emitting any logs due to buffering. 15 | ENV PYTHONUNBUFFERED=1 16 | 17 | # Required for the djnago-configuration library 18 | ENV DJANGO_SETTINGS_MODULE "src.settings" 19 | 20 | WORKDIR /app 21 | 22 | # Create a non-privileged user that the app will run under. 23 | # See https://docs.docker.com/go/dockerfile-user-best-practices/ 24 | ARG UID=10001 25 | RUN adduser \ 26 | --disabled-password \ 27 | --gecos "" \ 28 | --home "/nonexistent" \ 29 | --shell "/sbin/nologin" \ 30 | --no-create-home \ 31 | --uid "${UID}" \ 32 | django 33 | 34 | # Download dependencies as a separate step to take advantage of Docker's caching. 35 | # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. 36 | # Leverage a bind mount to requirements.txt to avoid having to copy them into 37 | # into this layer. 38 | RUN --mount=type=cache,target=/root/.cache/pip \ 39 | --mount=type=bind,source=requirements.txt,target=requirements.txt \ 40 | python -m pip install -r requirements.txt 41 | 42 | # Creating folder for gunicorn logs. 43 | RUN mkdir /var/log/gunicorn 44 | 45 | # Granting log permissions to non-privileged user. 46 | RUN chown -R django:django /var/log/gunicorn 47 | 48 | # Switch to the non-privileged user to run the application. 49 | USER django 50 | 51 | # Copy the source code into the container. 52 | COPY --chown=django:django . . 53 | 54 | # Expose the port that the application listens on. 55 | EXPOSE 8002 56 | 57 | # Run the application. 58 | CMD ["gunicorn", "-c", "/app/gunicorn.conf.py", "src.wsgi"] 59 | -------------------------------------------------------------------------------- /docker/services/payment/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RAKT Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker/services/payment/README.md: -------------------------------------------------------------------------------- 1 | # 👋 Hi, I'm Template -------------------------------------------------------------------------------- /docker/services/payment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/payment/__init__.py -------------------------------------------------------------------------------- /docker/services/payment/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | payment-service: 3 | build: 4 | context: . 5 | ports: 6 | - 8002:8002 7 | volumes: 8 | - .:/app 9 | env_file: 10 | - .env 11 | networks: 12 | - saga 13 | depends_on: 14 | payment-db: 15 | condition: service_healthy 16 | payment-publish: 17 | build: 18 | context: . 19 | command: [ "python", "manage.py", "publish" ] 20 | volumes: 21 | - .:/app 22 | env_file: 23 | - .env 24 | networks: 25 | - saga 26 | depends_on: 27 | payment-db: 28 | condition: service_healthy 29 | payment-subscribe: 30 | build: 31 | context: . 32 | command: [ "python", "manage.py", "subscribe", "src.core.callback.callback", "/exchange/saga/payment.v1", "payment.v1" ] 33 | volumes: 34 | - .:/app 35 | env_file: 36 | - .env 37 | networks: 38 | - saga 39 | depends_on: 40 | payment-db: 41 | condition: service_healthy 42 | payment-db: 43 | image: postgres:16 44 | platform: linux/amd64 45 | restart: always 46 | volumes: 47 | - payment-db-data:/var/lib/postgresql/data 48 | env_file: 49 | - .env 50 | expose: 51 | - 5432 52 | healthcheck: 53 | test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-db}" ] 54 | interval: 5s 55 | timeout: 5s 56 | retries: 5 57 | networks: 58 | - saga 59 | volumes: 60 | payment-db-data: 61 | 62 | networks: 63 | saga: 64 | driver: bridge -------------------------------------------------------------------------------- /docker/services/payment/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | # Bind to 0.0.0.0:8002 (accept connections from outside the container) 4 | bind = "0.0.0.0:8002" 5 | 6 | # Number of workers based on available CPUs in the container 7 | workers = multiprocessing.cpu_count() * 2 + 1 8 | 9 | # Worker class selection based on application type (asynchronous or not) 10 | worker_class = "gthread" # Or 'gevent' for asynchronous applications 11 | 12 | # Threads per worker for I/O intensive operations 13 | threads = 4 14 | 15 | # Timeout for requests 16 | timeout = 30 17 | 18 | # Worker temporary directory for better performance 19 | worker_tmp_dir = "/dev/shm" 20 | 21 | # Log for access and error 22 | logconfig_dict = { 23 | "version": 1, 24 | "formatters": { 25 | "json": { 26 | "()": "pythonjsonlogger.jsonlogger.JsonFormatter", 27 | "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d", 28 | }, 29 | }, 30 | "handlers": { 31 | "access": { 32 | "class": "logging.FileHandler", 33 | "filename": "/var/log/gunicorn/access.log", 34 | "formatter": "json", 35 | }, 36 | "error": { 37 | "class": "logging.FileHandler", 38 | "filename": "/var/log/gunicorn/error.log", 39 | "formatter": "json", 40 | }, 41 | }, 42 | "root": {"level": "INFO", "handlers": []}, 43 | "loggers": { 44 | "gunicorn.access": { 45 | "level": "INFO", 46 | "handlers": ["access"], 47 | "propagate": False, 48 | }, 49 | "gunicorn.error": { 50 | "level": "INFO", 51 | "handlers": ["error"], 52 | "propagate": False, 53 | }, 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /docker/services/payment/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /docker/services/payment/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "template" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Hugo Brilhante "] 6 | readme = "README.md" 7 | packages = [{include = "template", from = "src"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "3.12.1" 11 | django = "^5.0.1" 12 | django-configurations = {extras = ["cache", "database", "email", "search"], version = "^2.5"} 13 | whitenoise = "^6.6.0" 14 | gunicorn = "^21.2.0" 15 | psycopg2-binary = "^2.9.9" 16 | python-json-logger = "^2.0.7" 17 | djangorestframework = "^3.14.0" 18 | django-outbox-pattern = "^1.0.1" 19 | 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | pre-commit = "3.5.0" 23 | 24 | [tool.ruff.lint.isort] 25 | force-single-line = true 26 | 27 | [build-system] 28 | requires = ["poetry-core"] 29 | build-backend = "poetry.core.masonry.api" 30 | -------------------------------------------------------------------------------- /docker/services/payment/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 ; python_full_version == "3.12.1" \ 2 | --hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \ 3 | --hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed 4 | dj-database-url==2.1.0 ; python_full_version == "3.12.1" \ 5 | --hash=sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0 \ 6 | --hash=sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f 7 | dj-email-url==1.0.6 ; python_full_version == "3.12.1" \ 8 | --hash=sha256:55ffe3329e48f54f8a75aa36ece08f365e09d61f8a209773ef09a1d4760e699a \ 9 | --hash=sha256:cbd08327fbb08b104eac160fb4703f375532e4c0243eb230f5b960daee7a96db 10 | dj-search-url==0.1 ; python_full_version == "3.12.1" \ 11 | --hash=sha256:424d1a5852500b3c118abfdd0e30b3e0016fe68e7ed27b8553a67afa20d4fb40 12 | django-cache-url==3.4.5 ; python_full_version == "3.12.1" \ 13 | --hash=sha256:5f350759978483ab85dc0e3e17b3d53eed3394a28148f6bf0f53d11d0feb5b3c \ 14 | --hash=sha256:eb9fb194717524348c95cad9905b70b647452741c1d9e481fac6d2125f0ad917 15 | django-configurations[cache,database,email,search]==2.5 ; python_full_version == "3.12.1" \ 16 | --hash=sha256:63fa252c40dc88ea17b8b90f5f4a31a2726e586acb1ff0edc74c228c61f19e5d \ 17 | --hash=sha256:cf063b99ad30013df49eaa971bd8543deffb008ff080cf3a92955dbccfe81a5c 18 | django-outbox-pattern==1.0.1 ; python_full_version == "3.12.1" \ 19 | --hash=sha256:9db96fe073c4258ff0f992b454a4815fa06709defd01008d67f7d74b7392cd06 \ 20 | --hash=sha256:ae04163a42d1e9d156a0a03a720f3e20795082df37277fa185db5b1fd95029c5 21 | django==5.0.1 ; python_full_version == "3.12.1" \ 22 | --hash=sha256:8c8659665bc6e3a44fefe1ab0a291e5a3fb3979f9a8230be29de975e57e8f854 \ 23 | --hash=sha256:f47a37a90b9bbe2c8ec360235192c7fddfdc832206fcf618bb849b39256affc1 24 | djangorestframework==3.14.0 ; python_full_version == "3.12.1" \ 25 | --hash=sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8 \ 26 | --hash=sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08 27 | docopt==0.6.2 ; python_full_version == "3.12.1" \ 28 | --hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491 29 | gunicorn==21.2.0 ; python_full_version == "3.12.1" \ 30 | --hash=sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0 \ 31 | --hash=sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033 32 | packaging==23.2 ; python_full_version == "3.12.1" \ 33 | --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ 34 | --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 35 | psycopg2-binary==2.9.9 ; python_full_version == "3.12.1" \ 36 | --hash=sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9 \ 37 | --hash=sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77 \ 38 | --hash=sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e \ 39 | --hash=sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84 \ 40 | --hash=sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3 \ 41 | --hash=sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2 \ 42 | --hash=sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67 \ 43 | --hash=sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876 \ 44 | --hash=sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152 \ 45 | --hash=sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f \ 46 | --hash=sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a \ 47 | --hash=sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6 \ 48 | --hash=sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503 \ 49 | --hash=sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f \ 50 | --hash=sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493 \ 51 | --hash=sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996 \ 52 | --hash=sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f \ 53 | --hash=sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e \ 54 | --hash=sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59 \ 55 | --hash=sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94 \ 56 | --hash=sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7 \ 57 | --hash=sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682 \ 58 | --hash=sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420 \ 59 | --hash=sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae \ 60 | --hash=sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291 \ 61 | --hash=sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe \ 62 | --hash=sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980 \ 63 | --hash=sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93 \ 64 | --hash=sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692 \ 65 | --hash=sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119 \ 66 | --hash=sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716 \ 67 | --hash=sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472 \ 68 | --hash=sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b \ 69 | --hash=sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2 \ 70 | --hash=sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc \ 71 | --hash=sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c \ 72 | --hash=sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5 \ 73 | --hash=sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab \ 74 | --hash=sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984 \ 75 | --hash=sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9 \ 76 | --hash=sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf \ 77 | --hash=sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0 \ 78 | --hash=sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f \ 79 | --hash=sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212 \ 80 | --hash=sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb \ 81 | --hash=sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be \ 82 | --hash=sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90 \ 83 | --hash=sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041 \ 84 | --hash=sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7 \ 85 | --hash=sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860 \ 86 | --hash=sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d \ 87 | --hash=sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245 \ 88 | --hash=sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27 \ 89 | --hash=sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417 \ 90 | --hash=sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359 \ 91 | --hash=sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202 \ 92 | --hash=sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0 \ 93 | --hash=sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7 \ 94 | --hash=sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba \ 95 | --hash=sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1 \ 96 | --hash=sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd \ 97 | --hash=sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07 \ 98 | --hash=sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98 \ 99 | --hash=sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55 \ 100 | --hash=sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d \ 101 | --hash=sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972 \ 102 | --hash=sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f \ 103 | --hash=sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e \ 104 | --hash=sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26 \ 105 | --hash=sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957 \ 106 | --hash=sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53 \ 107 | --hash=sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52 108 | python-json-logger==2.0.7 ; python_full_version == "3.12.1" \ 109 | --hash=sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c \ 110 | --hash=sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd 111 | pytz==2023.3.post1 ; python_full_version == "3.12.1" \ 112 | --hash=sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b \ 113 | --hash=sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7 114 | sqlparse==0.4.4 ; python_full_version == "3.12.1" \ 115 | --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ 116 | --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c 117 | stomp-py==8.1.0 ; python_full_version == "3.12.1" \ 118 | --hash=sha256:49bc4cf5a4b17b6dce6dc2da0cb869f6522f77eff5ba46ee3e26ceb4793be0df \ 119 | --hash=sha256:b4737d002684639753fbf12dedecb8aa85e5e260749c947acd49c482db42a594 120 | typing-extensions==4.9.0 ; python_full_version == "3.12.1" \ 121 | --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ 122 | --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd 123 | tzdata==2023.4 ; sys_platform == "win32" and python_full_version == "3.12.1" \ 124 | --hash=sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3 \ 125 | --hash=sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9 126 | websocket-client==1.7.0 ; python_full_version == "3.12.1" \ 127 | --hash=sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6 \ 128 | --hash=sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588 129 | whitenoise==6.6.0 ; python_full_version == "3.12.1" \ 130 | --hash=sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251 \ 131 | --hash=sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146 132 | -------------------------------------------------------------------------------- /docker/services/payment/src/__init__.py: -------------------------------------------------------------------------------- 1 | from configurations import importer 2 | 3 | importer.install(check_options=True) 4 | -------------------------------------------------------------------------------- /docker/services/payment/src/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for src project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 16 | 17 | application = get_asgi_application() 18 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/payment/src/core/__init__.py -------------------------------------------------------------------------------- /docker/services/payment/src/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Payment 3 | 4 | admin.site.register(Payment) 5 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "src.core" 7 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/callback.py: -------------------------------------------------------------------------------- 1 | from django.db import transaction 2 | from django_outbox_pattern.payloads import Payload 3 | 4 | from .models import Payment 5 | 6 | PAYMENT_CONFIRMED = "payment_confirmed" 7 | PAYMENT_DENIED = "payment_denied" 8 | RESERVED = "reserved" 9 | 10 | 11 | def callback(payload: Payload): 12 | customer_id = payload.body["customer_id"] 13 | order_id = payload.body["order_id"] 14 | amount = payload.body["amount"] 15 | status = payload.body["status"] 16 | with transaction.atomic(): 17 | if status == RESERVED: 18 | payment = Payment(amount=amount, customer_id=customer_id, order_id=order_id) 19 | if amount < "1000": 20 | payment.status = PAYMENT_CONFIRMED 21 | else: 22 | payment.status = PAYMENT_DENIED 23 | payment.save() 24 | payload.save() 25 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/fixtures/payment.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$600000$HDxNWYr1aaj3uizUw73YAu$BflQx1Hl0BmCTeDawhjVBuqDbaIM4VJOCSfMKB+G17g=", 7 | "last_login": "2024-01-18T17:31:49.328Z", 8 | "is_superuser": true, 9 | "username": "admin", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "admin@admin.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2024-01-18T17:30:18.484Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /docker/services/payment/src/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-01-19 01:39 2 | 3 | from django.db import migrations, models 4 | import uuid 5 | 6 | 7 | class Migration(migrations.Migration): 8 | initial = True 9 | 10 | dependencies = [] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="Payment", 15 | fields=[ 16 | ( 17 | "payment_id", 18 | models.UUIDField( 19 | default=uuid.uuid4, 20 | editable=False, 21 | primary_key=True, 22 | serialize=False, 23 | ), 24 | ), 25 | ("customer_id", models.CharField(max_length=20)), 26 | ("order_id", models.CharField(max_length=20)), 27 | ("amount", models.CharField(max_length=20)), 28 | ( 29 | "status", 30 | models.CharField( 31 | choices=[ 32 | ("payment_confirmed", "Payment Confirmed"), 33 | ("payment_denied", "Payment Denied"), 34 | ], 35 | max_length=20, 36 | ), 37 | ), 38 | ("created", models.DateTimeField(auto_now_add=True)), 39 | ], 40 | ), 41 | ] 42 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/payment/src/core/migrations/__init__.py -------------------------------------------------------------------------------- /docker/services/payment/src/core/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from django.db import models 4 | from django_outbox_pattern.decorators import Config 5 | from django_outbox_pattern.decorators import publish 6 | 7 | 8 | @publish( 9 | [ 10 | Config( 11 | destination="/exchange/saga/order", 12 | version="v1", 13 | serializer="payment_serializer", 14 | ) 15 | ] 16 | ) 17 | class Payment(models.Model): 18 | STATUS_CHOICES = ( 19 | ("payment_confirmed", "Payment Confirmed"), 20 | ("payment_denied", "Payment Denied"), 21 | ) 22 | payment_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) 23 | customer_id = models.CharField(max_length=20) 24 | order_id = models.CharField(max_length=20) 25 | amount = models.CharField(max_length=20) 26 | status = models.CharField(max_length=20, choices=STATUS_CHOICES) 27 | created = models.DateTimeField(auto_now_add=True) 28 | 29 | def payment_serializer(self): 30 | return {"order_id": self.order_id, "status": self.status} 31 | 32 | def __str__(self): 33 | return f"Payment to Order: {self.order_id} - Amount: {self.amount} - Status: {self.status}" 34 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Payment 3 | 4 | 5 | class PaymentSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Payment 8 | fields = "__all__" 9 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import PaymentListView 4 | 5 | 6 | urlpatterns = [ 7 | path("api/v1/payments/", PaymentListView.as_view(), name="payment-list-create"), 8 | ] 9 | -------------------------------------------------------------------------------- /docker/services/payment/src/core/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Payment 3 | from .serializers import PaymentSerializer 4 | 5 | 6 | class PaymentListView(generics.ListAPIView): 7 | queryset = Payment.objects.all() 8 | serializer_class = PaymentSerializer 9 | -------------------------------------------------------------------------------- /docker/services/payment/src/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for src project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | import os 13 | import sys 14 | 15 | from pathlib import Path 16 | 17 | from configurations import Configuration, values 18 | 19 | 20 | class Base(Configuration): 21 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 22 | BASE_DIR = Path(__file__).resolve().parent.parent 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = values.Value() 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = values.BooleanValue(False) 32 | 33 | ALLOWED_HOSTS = values.ListValue([]) 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | "django.contrib.admin", 39 | "django.contrib.auth", 40 | "django.contrib.contenttypes", 41 | "django.contrib.sessions", 42 | "django.contrib.messages", 43 | "django.contrib.staticfiles", 44 | # Installed Apps 45 | "rest_framework", 46 | "django_outbox_pattern", 47 | # App locals 48 | "src.core.apps.CoreConfig", 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | "django.middleware.security.SecurityMiddleware", 53 | "whitenoise.middleware.WhiteNoiseMiddleware", 54 | "django.contrib.sessions.middleware.SessionMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.messages.middleware.MessageMiddleware", 59 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 60 | ] 61 | 62 | ROOT_URLCONF = "src.urls" 63 | 64 | TEMPLATES = [ 65 | { 66 | "BACKEND": "django.template.backends.django.DjangoTemplates", 67 | "DIRS": [], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = "src.wsgi.application" 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 84 | 85 | DATABASES = values.DatabaseURLValue( 86 | "sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3") 87 | ) 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 104 | }, 105 | ] 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 109 | 110 | LANGUAGE_CODE = values.Value("en-us") 111 | 112 | TIME_ZONE = values.Value("UTC") 113 | 114 | USE_I18N = values.BooleanValue(True) 115 | 116 | USE_TZ = values.BooleanValue(True) 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 120 | 121 | STATIC_URL = "/static/" 122 | 123 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 124 | 125 | STORAGES = { 126 | "staticfiles": { 127 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 128 | }, 129 | } 130 | 131 | # See https://docs.djangoproject.com/en/4.2/topics/email/#console-backend 132 | EMAIL = values.EmailURLValue("console://") 133 | 134 | # Default primary key field type 135 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 136 | 137 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 138 | 139 | DJANGO_OUTBOX_PATTERN = { 140 | "DEFAULT_STOMP_HOST_AND_PORTS": [("rabbitmq", 61613)], 141 | } 142 | 143 | 144 | class Dev(Base): 145 | # See https://docs.djangoproject.com/en/4.2/topics/cache/#dummy-caching-for-development 146 | CACHES = values.CacheURLValue("dummy://") 147 | # See http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development 148 | Base.INSTALLED_APPS.insert(0, "whitenoise.runserver_nostatic") 149 | 150 | 151 | class Prod(Base): 152 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/#csrf-cookie-secure 153 | CSRF_COOKIE_SECURE = True 154 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/#session-cookie-secure 155 | SESSION_COOKIE_SECURE = True 156 | 157 | LOGGING = { 158 | "version": 1, 159 | "disable_existing_loggers": False, 160 | "formatters": { 161 | "json": { 162 | "()": "pythonjsonlogger.jsonlogger.JsonFormatter", 163 | "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d", 164 | }, 165 | }, 166 | "handlers": { 167 | "stdout": { 168 | "level": "INFO", 169 | "class": "logging.StreamHandler", 170 | "stream": sys.stdout, 171 | "formatter": "json", 172 | }, 173 | }, 174 | "loggers": { 175 | "": { 176 | "handlers": ["stdout"], 177 | "level": "INFO", 178 | "propagate": True, 179 | }, 180 | }, 181 | } 182 | -------------------------------------------------------------------------------- /docker/services/payment/src/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [path("admin/", admin.site.urls), path("", include("src.core.urls"))] 5 | -------------------------------------------------------------------------------- /docker/services/payment/src/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for src project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /docker/services/payment/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/payment/tests/__init__.py -------------------------------------------------------------------------------- /docker/services/stock/.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 6 | 7 | **/.DS_Store 8 | **/__pycache__ 9 | **/.venv 10 | **/.classpath 11 | **/.dockerignore 12 | **/.env 13 | **/.git 14 | **/.gitignore 15 | **/.project 16 | **/.settings 17 | **/.toolstarget 18 | **/.vs 19 | **/.vscode 20 | **/*.*proj.user 21 | **/*.dbmdl 22 | **/*.jfm 23 | **/bin 24 | **/charts 25 | **/docker-compose* 26 | **/compose* 27 | **/Dockerfile* 28 | **/node_modules 29 | **/npm-debug.log 30 | **/obj 31 | **/secrets.dev.yaml 32 | **/values.dev.yaml 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /docker/services/stock/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = false 9 | max_line_length = 120 10 | tab_width = 4 11 | 12 | [{*.yaml,*.yml}] 13 | indent_size = 2 -------------------------------------------------------------------------------- /docker/services/stock/.env.example: -------------------------------------------------------------------------------- 1 | DJANGO_SECRET_KEY='secret_key' 2 | DJANGO_DEBUG=True 3 | DJANGO_ALLOWED_HOSTS=* 4 | DATABASE_URL=postgres://user:password@stock-db:5432/db 5 | DJANGO_LANGUAGE_CODE=en-us 6 | DJANGO_TIME_ZONE=UTC 7 | DJANGO_USE_I18N=True 8 | DJANGO_USE_TZ=True 9 | DJANGO_CONFIGURATION=Dev 10 | POSTGRES_DB=db 11 | POSTGRES_USER=user 12 | POSTGRES_PASSWORD=password -------------------------------------------------------------------------------- /docker/services/stock/.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | 4 | # Ruff 5 | .ruff_cache 6 | 7 | # Envs 8 | .env 9 | .venv 10 | 11 | # Staticfiles 12 | staticfiles 13 | 14 | # Logs 15 | *.log -------------------------------------------------------------------------------- /docker/services/stock/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: 'v0.1.14' 4 | hooks: 5 | - id: ruff 6 | args: [ --fix ] 7 | - id: ruff-format 8 | - repo: https://github.com/python-poetry/poetry 9 | rev: '1.7.0' 10 | hooks: 11 | - id: poetry-check 12 | - id: poetry-lock 13 | - id: poetry-export 14 | args: ["-f", "requirements.txt", "-o", "requirements.txt"] 15 | - id: poetry-install -------------------------------------------------------------------------------- /docker/services/stock/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Comments are provided throughout this file to help you get started. 4 | # If you need more help, visit the Dockerfile reference guide at 5 | # https://docs.docker.com/engine/reference/builder/ 6 | 7 | ARG PYTHON_VERSION=3.12.1 8 | FROM python:${PYTHON_VERSION}-alpine as base 9 | 10 | # Prevents Python from writing pyc files. 11 | ENV PYTHONDONTWRITEBYTECODE=1 12 | 13 | # Keeps Python from buffering stdout and stderr to avoid situations where 14 | # the application crashes without emitting any logs due to buffering. 15 | ENV PYTHONUNBUFFERED=1 16 | 17 | # Required for the djnago-configuration library 18 | ENV DJANGO_SETTINGS_MODULE "src.settings" 19 | 20 | WORKDIR /app 21 | 22 | # Create a non-privileged user that the app will run under. 23 | # See https://docs.docker.com/go/dockerfile-user-best-practices/ 24 | ARG UID=10001 25 | RUN adduser \ 26 | --disabled-password \ 27 | --gecos "" \ 28 | --home "/nonexistent" \ 29 | --shell "/sbin/nologin" \ 30 | --no-create-home \ 31 | --uid "${UID}" \ 32 | django 33 | 34 | # Download dependencies as a separate step to take advantage of Docker's caching. 35 | # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. 36 | # Leverage a bind mount to requirements.txt to avoid having to copy them into 37 | # into this layer. 38 | RUN --mount=type=cache,target=/root/.cache/pip \ 39 | --mount=type=bind,source=requirements.txt,target=requirements.txt \ 40 | python -m pip install -r requirements.txt 41 | 42 | # Creating folder for gunicorn logs. 43 | RUN mkdir /var/log/gunicorn 44 | 45 | # Granting log permissions to non-privileged user. 46 | RUN chown -R django:django /var/log/gunicorn 47 | 48 | # Switch to the non-privileged user to run the application. 49 | USER django 50 | 51 | # Copy the source code into the container. 52 | COPY --chown=django:django . . 53 | 54 | # Expose the port that the application listens on. 55 | EXPOSE 8001 56 | 57 | # Run the application. 58 | CMD ["gunicorn", "-c", "/app/gunicorn.conf.py", "src.wsgi"] 59 | -------------------------------------------------------------------------------- /docker/services/stock/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RAKT Innovations 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker/services/stock/README.md: -------------------------------------------------------------------------------- 1 | # 👋 Hi, I'm Template -------------------------------------------------------------------------------- /docker/services/stock/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/stock/__init__.py -------------------------------------------------------------------------------- /docker/services/stock/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | stock-service: 3 | build: 4 | context: . 5 | ports: 6 | - 8001:8001 7 | volumes: 8 | - .:/app 9 | env_file: 10 | - .env 11 | networks: 12 | - saga 13 | depends_on: 14 | stock-db: 15 | condition: service_healthy 16 | stock-publish: 17 | build: 18 | context: . 19 | command: [ "python", "manage.py", "publish" ] 20 | volumes: 21 | - .:/app 22 | env_file: 23 | - .env 24 | networks: 25 | - saga 26 | depends_on: 27 | stock-db: 28 | condition: service_healthy 29 | stock-subscribe: 30 | build: 31 | context: . 32 | command: [ "python", "manage.py", "subscribe", "src.core.callback.callback", "/exchange/saga/stock.v1", "stock.v1" ] 33 | volumes: 34 | - .:/app 35 | env_file: 36 | - .env 37 | networks: 38 | - saga 39 | depends_on: 40 | stock-db: 41 | condition: service_healthy 42 | stock-db: 43 | image: postgres:16 44 | platform: linux/amd64 45 | restart: always 46 | volumes: 47 | - stock-db-data:/var/lib/postgresql/data 48 | env_file: 49 | - .env 50 | expose: 51 | - 5432 52 | healthcheck: 53 | test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-db}" ] 54 | interval: 5s 55 | timeout: 5s 56 | retries: 5 57 | networks: 58 | - saga 59 | volumes: 60 | stock-db-data: 61 | 62 | networks: 63 | saga: 64 | driver: bridge -------------------------------------------------------------------------------- /docker/services/stock/gunicorn.conf.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | 3 | # Bind to 0.0.0.0:8001 (accept connections from outside the container) 4 | bind = "0.0.0.0:8001" 5 | 6 | # Number of workers based on available CPUs in the container 7 | workers = multiprocessing.cpu_count() * 2 + 1 8 | 9 | # Worker class selection based on application type (asynchronous or not) 10 | worker_class = "gthread" # Or 'gevent' for asynchronous applications 11 | 12 | # Threads per worker for I/O intensive operations 13 | threads = 4 14 | 15 | # Timeout for requests 16 | timeout = 30 17 | 18 | # Worker temporary directory for better performance 19 | worker_tmp_dir = "/dev/shm" 20 | 21 | # Log for access and error 22 | logconfig_dict = { 23 | "version": 1, 24 | "formatters": { 25 | "json": { 26 | "()": "pythonjsonlogger.jsonlogger.JsonFormatter", 27 | "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d", 28 | }, 29 | }, 30 | "handlers": { 31 | "access": { 32 | "class": "logging.FileHandler", 33 | "filename": "/var/log/gunicorn/access.log", 34 | "formatter": "json", 35 | }, 36 | "error": { 37 | "class": "logging.FileHandler", 38 | "filename": "/var/log/gunicorn/error.log", 39 | "formatter": "json", 40 | }, 41 | }, 42 | "root": {"level": "INFO", "handlers": []}, 43 | "loggers": { 44 | "gunicorn.access": { 45 | "level": "INFO", 46 | "handlers": ["access"], 47 | "propagate": False, 48 | }, 49 | "gunicorn.error": { 50 | "level": "INFO", 51 | "handlers": ["error"], 52 | "propagate": False, 53 | }, 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /docker/services/stock/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 10 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 11 | try: 12 | from django.core.management import execute_from_command_line 13 | except ImportError as exc: 14 | raise ImportError( 15 | "Couldn't import Django. Are you sure it's installed and " 16 | "available on your PYTHONPATH environment variable? Did you " 17 | "forget to activate a virtual environment?" 18 | ) from exc 19 | execute_from_command_line(sys.argv) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /docker/services/stock/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "template" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Hugo Brilhante "] 6 | readme = "README.md" 7 | packages = [{include = "template", from = "src"}] 8 | 9 | [tool.poetry.dependencies] 10 | python = "3.12.1" 11 | django = "^5.0.1" 12 | django-configurations = {extras = ["cache", "database", "email", "search"], version = "^2.5"} 13 | whitenoise = "^6.6.0" 14 | gunicorn = "^21.2.0" 15 | psycopg2-binary = "^2.9.9" 16 | python-json-logger = "^2.0.7" 17 | djangorestframework = "^3.14.0" 18 | django-outbox-pattern = "^1.0.1" 19 | 20 | 21 | [tool.poetry.group.dev.dependencies] 22 | pre-commit = "3.5.0" 23 | 24 | [tool.ruff.lint.isort] 25 | force-single-line = true 26 | 27 | [build-system] 28 | requires = ["poetry-core"] 29 | build-backend = "poetry.core.masonry.api" 30 | -------------------------------------------------------------------------------- /docker/services/stock/requirements.txt: -------------------------------------------------------------------------------- 1 | asgiref==3.7.2 ; python_full_version == "3.12.1" \ 2 | --hash=sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e \ 3 | --hash=sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed 4 | dj-database-url==2.1.0 ; python_full_version == "3.12.1" \ 5 | --hash=sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0 \ 6 | --hash=sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f 7 | dj-email-url==1.0.6 ; python_full_version == "3.12.1" \ 8 | --hash=sha256:55ffe3329e48f54f8a75aa36ece08f365e09d61f8a209773ef09a1d4760e699a \ 9 | --hash=sha256:cbd08327fbb08b104eac160fb4703f375532e4c0243eb230f5b960daee7a96db 10 | dj-search-url==0.1 ; python_full_version == "3.12.1" \ 11 | --hash=sha256:424d1a5852500b3c118abfdd0e30b3e0016fe68e7ed27b8553a67afa20d4fb40 12 | django-cache-url==3.4.5 ; python_full_version == "3.12.1" \ 13 | --hash=sha256:5f350759978483ab85dc0e3e17b3d53eed3394a28148f6bf0f53d11d0feb5b3c \ 14 | --hash=sha256:eb9fb194717524348c95cad9905b70b647452741c1d9e481fac6d2125f0ad917 15 | django-configurations[cache,database,email,search]==2.5 ; python_full_version == "3.12.1" \ 16 | --hash=sha256:63fa252c40dc88ea17b8b90f5f4a31a2726e586acb1ff0edc74c228c61f19e5d \ 17 | --hash=sha256:cf063b99ad30013df49eaa971bd8543deffb008ff080cf3a92955dbccfe81a5c 18 | django-outbox-pattern==1.0.1 ; python_full_version == "3.12.1" \ 19 | --hash=sha256:9db96fe073c4258ff0f992b454a4815fa06709defd01008d67f7d74b7392cd06 \ 20 | --hash=sha256:ae04163a42d1e9d156a0a03a720f3e20795082df37277fa185db5b1fd95029c5 21 | django==5.0.1 ; python_full_version == "3.12.1" \ 22 | --hash=sha256:8c8659665bc6e3a44fefe1ab0a291e5a3fb3979f9a8230be29de975e57e8f854 \ 23 | --hash=sha256:f47a37a90b9bbe2c8ec360235192c7fddfdc832206fcf618bb849b39256affc1 24 | djangorestframework==3.14.0 ; python_full_version == "3.12.1" \ 25 | --hash=sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8 \ 26 | --hash=sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08 27 | docopt==0.6.2 ; python_full_version == "3.12.1" \ 28 | --hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491 29 | gunicorn==21.2.0 ; python_full_version == "3.12.1" \ 30 | --hash=sha256:3213aa5e8c24949e792bcacfc176fef362e7aac80b76c56f6b5122bf350722f0 \ 31 | --hash=sha256:88ec8bff1d634f98e61b9f65bc4bf3cd918a90806c6f5c48bc5603849ec81033 32 | packaging==23.2 ; python_full_version == "3.12.1" \ 33 | --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ 34 | --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 35 | psycopg2-binary==2.9.9 ; python_full_version == "3.12.1" \ 36 | --hash=sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9 \ 37 | --hash=sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77 \ 38 | --hash=sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e \ 39 | --hash=sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84 \ 40 | --hash=sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3 \ 41 | --hash=sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2 \ 42 | --hash=sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67 \ 43 | --hash=sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876 \ 44 | --hash=sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152 \ 45 | --hash=sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f \ 46 | --hash=sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a \ 47 | --hash=sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6 \ 48 | --hash=sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503 \ 49 | --hash=sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f \ 50 | --hash=sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493 \ 51 | --hash=sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996 \ 52 | --hash=sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f \ 53 | --hash=sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e \ 54 | --hash=sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59 \ 55 | --hash=sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94 \ 56 | --hash=sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7 \ 57 | --hash=sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682 \ 58 | --hash=sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420 \ 59 | --hash=sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae \ 60 | --hash=sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291 \ 61 | --hash=sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe \ 62 | --hash=sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980 \ 63 | --hash=sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93 \ 64 | --hash=sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692 \ 65 | --hash=sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119 \ 66 | --hash=sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716 \ 67 | --hash=sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472 \ 68 | --hash=sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b \ 69 | --hash=sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2 \ 70 | --hash=sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc \ 71 | --hash=sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c \ 72 | --hash=sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5 \ 73 | --hash=sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab \ 74 | --hash=sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984 \ 75 | --hash=sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9 \ 76 | --hash=sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf \ 77 | --hash=sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0 \ 78 | --hash=sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f \ 79 | --hash=sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212 \ 80 | --hash=sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb \ 81 | --hash=sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be \ 82 | --hash=sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90 \ 83 | --hash=sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041 \ 84 | --hash=sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7 \ 85 | --hash=sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860 \ 86 | --hash=sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d \ 87 | --hash=sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245 \ 88 | --hash=sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27 \ 89 | --hash=sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417 \ 90 | --hash=sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359 \ 91 | --hash=sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202 \ 92 | --hash=sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0 \ 93 | --hash=sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7 \ 94 | --hash=sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba \ 95 | --hash=sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1 \ 96 | --hash=sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd \ 97 | --hash=sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07 \ 98 | --hash=sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98 \ 99 | --hash=sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55 \ 100 | --hash=sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d \ 101 | --hash=sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972 \ 102 | --hash=sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f \ 103 | --hash=sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e \ 104 | --hash=sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26 \ 105 | --hash=sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957 \ 106 | --hash=sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53 \ 107 | --hash=sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52 108 | python-json-logger==2.0.7 ; python_full_version == "3.12.1" \ 109 | --hash=sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c \ 110 | --hash=sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd 111 | pytz==2023.3.post1 ; python_full_version == "3.12.1" \ 112 | --hash=sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b \ 113 | --hash=sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7 114 | sqlparse==0.4.4 ; python_full_version == "3.12.1" \ 115 | --hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \ 116 | --hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c 117 | stomp-py==8.1.0 ; python_full_version == "3.12.1" \ 118 | --hash=sha256:49bc4cf5a4b17b6dce6dc2da0cb869f6522f77eff5ba46ee3e26ceb4793be0df \ 119 | --hash=sha256:b4737d002684639753fbf12dedecb8aa85e5e260749c947acd49c482db42a594 120 | typing-extensions==4.9.0 ; python_full_version == "3.12.1" \ 121 | --hash=sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783 \ 122 | --hash=sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd 123 | tzdata==2023.4 ; sys_platform == "win32" and python_full_version == "3.12.1" \ 124 | --hash=sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3 \ 125 | --hash=sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9 126 | websocket-client==1.7.0 ; python_full_version == "3.12.1" \ 127 | --hash=sha256:10e511ea3a8c744631d3bd77e61eb17ed09304c413ad42cf6ddfa4c7787e8fe6 \ 128 | --hash=sha256:f4c3d22fec12a2461427a29957ff07d35098ee2d976d3ba244e688b8b4057588 129 | whitenoise==6.6.0 ; python_full_version == "3.12.1" \ 130 | --hash=sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251 \ 131 | --hash=sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146 132 | -------------------------------------------------------------------------------- /docker/services/stock/src/__init__.py: -------------------------------------------------------------------------------- 1 | from configurations import importer 2 | 3 | importer.install(check_options=True) 4 | -------------------------------------------------------------------------------- /docker/services/stock/src/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for src project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 16 | 17 | application = get_asgi_application() 18 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/stock/src/core/__init__.py -------------------------------------------------------------------------------- /docker/services/stock/src/core/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Reservation, Stock 3 | 4 | admin.site.register(Reservation) 5 | admin.site.register(Stock) 6 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CoreConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "src.core" 7 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/callback.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | 3 | from django.db import transaction 4 | from django_outbox_pattern.payloads import Payload 5 | 6 | from .models import Reservation, Stock 7 | 8 | CREATED = "created" 9 | PAYMENT_CONFIRMED = "payment_confirmed" 10 | PAYMENT_DENIED = "payment_denied" 11 | RESERVED = "reserved" 12 | NOT_RESERVED = "not_reserved" 13 | 14 | 15 | def callback(payload: Payload): 16 | product_id = payload.body["product_id"] 17 | order_id = payload.body["order_id"] 18 | quantity = payload.body["quantity"] 19 | status = payload.body["status"] 20 | with transaction.atomic(): 21 | stock = Stock.objects.get(product_id=product_id) 22 | try: 23 | reservation = Reservation.objects.get( 24 | product_id=product_id, order_id=order_id 25 | ) 26 | except Reservation.DoesNotExist: 27 | reservation = Reservation( 28 | product_id=product_id, order_id=order_id, quantity=quantity 29 | ) 30 | if status == CREATED: 31 | if quantity <= stock.quantity: 32 | stock.quantity -= quantity 33 | reservation.status = RESERVED 34 | else: 35 | reservation.status = NOT_RESERVED 36 | reservation.save() 37 | elif status == PAYMENT_DENIED: 38 | stock.quantity += quantity 39 | reservation.status = PAYMENT_DENIED 40 | reservation.save() 41 | elif status == PAYMENT_CONFIRMED: 42 | reservation.status = PAYMENT_CONFIRMED 43 | reservation.save() 44 | stock.save() 45 | payload.save() 46 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/fixtures/payment.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$600000$HDxNWYr1aaj3uizUw73YAu$BflQx1Hl0BmCTeDawhjVBuqDbaIM4VJOCSfMKB+G17g=", 7 | "last_login": "2024-01-18T17:31:49.328Z", 8 | "is_superuser": true, 9 | "username": "admin", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "admin@admin.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2024-01-18T17:30:18.484Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | } 20 | ] -------------------------------------------------------------------------------- /docker/services/stock/src/core/fixtures/stock.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "model": "auth.user", 4 | "pk": 1, 5 | "fields": { 6 | "password": "pbkdf2_sha256$600000$HDxNWYr1aaj3uizUw73YAu$BflQx1Hl0BmCTeDawhjVBuqDbaIM4VJOCSfMKB+G17g=", 7 | "last_login": "2024-01-18T17:31:49.328Z", 8 | "is_superuser": true, 9 | "username": "admin", 10 | "first_name": "", 11 | "last_name": "", 12 | "email": "admin@admin.com", 13 | "is_staff": true, 14 | "is_active": true, 15 | "date_joined": "2024-01-18T17:30:18.484Z", 16 | "groups": [], 17 | "user_permissions": [] 18 | } 19 | }, 20 | { 21 | "model": "core.stock", 22 | "pk": 1, 23 | "fields": { 24 | "product_id": "1", 25 | "quantity": 10 26 | } 27 | }, 28 | { 29 | "model": "core.stock", 30 | "pk": 2, 31 | "fields": { 32 | "product_id": "2", 33 | "quantity": 10 34 | } 35 | } 36 | ] -------------------------------------------------------------------------------- /docker/services/stock/src/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.8 on 2024-01-19 01:39 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name="Reservation", 14 | fields=[ 15 | ( 16 | "id", 17 | models.BigAutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name="ID", 22 | ), 23 | ), 24 | ("order_id", models.CharField(max_length=20)), 25 | ("product_id", models.CharField(max_length=20)), 26 | ("quantity", models.PositiveIntegerField(default=0)), 27 | ( 28 | "status", 29 | models.CharField( 30 | choices=[ 31 | ("reserved", "Reserved"), 32 | ("not_reserved", "Not Reserved"), 33 | ("payment_denied", "Payment Denied"), 34 | ], 35 | max_length=20, 36 | ), 37 | ), 38 | ], 39 | ), 40 | migrations.CreateModel( 41 | name="Stock", 42 | fields=[ 43 | ( 44 | "id", 45 | models.BigAutoField( 46 | auto_created=True, 47 | primary_key=True, 48 | serialize=False, 49 | verbose_name="ID", 50 | ), 51 | ), 52 | ("product_id", models.CharField(max_length=20)), 53 | ("quantity", models.PositiveIntegerField(default=0)), 54 | ], 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/stock/src/core/migrations/__init__.py -------------------------------------------------------------------------------- /docker/services/stock/src/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django_outbox_pattern.decorators import Config 3 | from django_outbox_pattern.decorators import publish 4 | 5 | 6 | class Stock(models.Model): 7 | product_id = models.CharField(max_length=20) 8 | quantity = models.PositiveIntegerField(default=0) 9 | 10 | def __str__(self): 11 | return f"Product {self.product_id} - Available: {self.quantity}" 12 | 13 | 14 | @publish( 15 | [ 16 | Config( 17 | destination="/exchange/saga/order", 18 | version="v1", 19 | serializer="reservation_serializer", 20 | ) 21 | ] 22 | ) 23 | class Reservation(models.Model): 24 | STATUS_CHOICES = ( 25 | ("reserved", "Reserved"), 26 | ("not_reserved", "Not Reserved"), 27 | ("payment_denied", "Payment Denied"), 28 | ("payment_confirmed", "Payment Confirmed") 29 | ) 30 | order_id = models.CharField(max_length=20) 31 | product_id = models.CharField(max_length=20) 32 | quantity = models.PositiveIntegerField(default=0) 33 | status = models.CharField(max_length=20, choices=STATUS_CHOICES) 34 | 35 | def reservation_serializer(self): 36 | return {"order_id": self.order_id, "status": self.status} 37 | 38 | def __str__(self): 39 | return f"Product {self.product_id} - Order: {self.order_id} - Quantity: {self.quantity} - Status: {self.status}" 40 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Reservation, Stock 3 | 4 | 5 | class ReservationSerializer(serializers.ModelSerializer): 6 | class Meta: 7 | model = Reservation 8 | fields = "__all__" 9 | 10 | 11 | class StockSerializer(serializers.ModelSerializer): 12 | class Meta: 13 | model = Stock 14 | fields = "__all__" 15 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from .views import ReservationListView, StockListView 4 | 5 | 6 | urlpatterns = [ 7 | path( 8 | "api/v1/reservations/", ReservationListView.as_view(), name="reservation-list" 9 | ), 10 | path("api/v1/stocks/", StockListView.as_view(), name="stock-list"), 11 | ] 12 | -------------------------------------------------------------------------------- /docker/services/stock/src/core/views.py: -------------------------------------------------------------------------------- 1 | from rest_framework import generics 2 | from .models import Reservation, Stock 3 | from .serializers import ReservationSerializer, StockSerializer 4 | 5 | 6 | class ReservationListView(generics.ListAPIView): 7 | queryset = Reservation.objects.all() 8 | serializer_class = ReservationSerializer 9 | 10 | 11 | class StockListView(generics.ListAPIView): 12 | queryset = Stock.objects.all() 13 | serializer_class = StockSerializer 14 | -------------------------------------------------------------------------------- /docker/services/stock/src/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for src project. 3 | 4 | Generated by 'django-admin startproject' using Django 4.2.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/4.2/ref/settings/ 11 | """ 12 | import os 13 | import sys 14 | 15 | from pathlib import Path 16 | 17 | from configurations import Configuration, values 18 | 19 | 20 | class Base(Configuration): 21 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 22 | BASE_DIR = Path(__file__).resolve().parent.parent 23 | 24 | # Quick-start development settings - unsuitable for production 25 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ 26 | 27 | # SECURITY WARNING: keep the secret key used in production secret! 28 | SECRET_KEY = values.Value() 29 | 30 | # SECURITY WARNING: don't run with debug turned on in production! 31 | DEBUG = values.BooleanValue(False) 32 | 33 | ALLOWED_HOSTS = values.ListValue([]) 34 | 35 | # Application definition 36 | 37 | INSTALLED_APPS = [ 38 | "django.contrib.admin", 39 | "django.contrib.auth", 40 | "django.contrib.contenttypes", 41 | "django.contrib.sessions", 42 | "django.contrib.messages", 43 | "django.contrib.staticfiles", 44 | # Installed Apps 45 | "rest_framework", 46 | "django_outbox_pattern", 47 | # App locals 48 | "src.core.apps.CoreConfig", 49 | ] 50 | 51 | MIDDLEWARE = [ 52 | "django.middleware.security.SecurityMiddleware", 53 | "whitenoise.middleware.WhiteNoiseMiddleware", 54 | "django.contrib.sessions.middleware.SessionMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.messages.middleware.MessageMiddleware", 59 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 60 | ] 61 | 62 | ROOT_URLCONF = "src.urls" 63 | 64 | TEMPLATES = [ 65 | { 66 | "BACKEND": "django.template.backends.django.DjangoTemplates", 67 | "DIRS": [], 68 | "APP_DIRS": True, 69 | "OPTIONS": { 70 | "context_processors": [ 71 | "django.template.context_processors.debug", 72 | "django.template.context_processors.request", 73 | "django.contrib.auth.context_processors.auth", 74 | "django.contrib.messages.context_processors.messages", 75 | ], 76 | }, 77 | }, 78 | ] 79 | 80 | WSGI_APPLICATION = "src.wsgi.application" 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/4.2/ref/settings/#databases 84 | 85 | DATABASES = values.DatabaseURLValue( 86 | "sqlite:///" + os.path.join(BASE_DIR, "db.sqlite3") 87 | ) 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 95 | }, 96 | { 97 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 98 | }, 99 | { 100 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 101 | }, 102 | { 103 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 104 | }, 105 | ] 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/4.2/topics/i18n/ 109 | 110 | LANGUAGE_CODE = values.Value("en-us") 111 | 112 | TIME_ZONE = values.Value("UTC") 113 | 114 | USE_I18N = values.BooleanValue(True) 115 | 116 | USE_TZ = values.BooleanValue(True) 117 | 118 | # Static files (CSS, JavaScript, Images) 119 | # https://docs.djangoproject.com/en/4.2/howto/static-files/ 120 | 121 | STATIC_URL = "/static/" 122 | 123 | STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") 124 | 125 | STORAGES = { 126 | "staticfiles": { 127 | "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", 128 | }, 129 | } 130 | 131 | # See https://docs.djangoproject.com/en/4.2/topics/email/#console-backend 132 | EMAIL = values.EmailURLValue("console://") 133 | 134 | # Default primary key field type 135 | # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field 136 | 137 | DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 138 | 139 | DJANGO_OUTBOX_PATTERN = { 140 | "DEFAULT_STOMP_HOST_AND_PORTS": [("rabbitmq", 61613)], 141 | } 142 | 143 | 144 | class Dev(Base): 145 | # See https://docs.djangoproject.com/en/4.2/topics/cache/#dummy-caching-for-development 146 | CACHES = values.CacheURLValue("dummy://") 147 | # See http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development 148 | Base.INSTALLED_APPS.insert(0, "whitenoise.runserver_nostatic") 149 | 150 | 151 | class Prod(Base): 152 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/#csrf-cookie-secure 153 | CSRF_COOKIE_SECURE = True 154 | # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/#session-cookie-secure 155 | SESSION_COOKIE_SECURE = True 156 | 157 | LOGGING = { 158 | "version": 1, 159 | "disable_existing_loggers": False, 160 | "formatters": { 161 | "json": { 162 | "()": "pythonjsonlogger.jsonlogger.JsonFormatter", 163 | "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(pathname)s %(lineno)d", 164 | }, 165 | }, 166 | "handlers": { 167 | "stdout": { 168 | "level": "INFO", 169 | "class": "logging.StreamHandler", 170 | "stream": sys.stdout, 171 | "formatter": "json", 172 | }, 173 | }, 174 | "loggers": { 175 | "": { 176 | "handlers": ["stdout"], 177 | "level": "INFO", 178 | "propagate": True, 179 | }, 180 | }, 181 | } 182 | -------------------------------------------------------------------------------- /docker/services/stock/src/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path, include 3 | 4 | urlpatterns = [path("admin/", admin.site.urls), path("", include("src.core.urls"))] 5 | -------------------------------------------------------------------------------- /docker/services/stock/src/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for src project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "src.settings") 15 | os.environ.setdefault("DJANGO_CONFIGURATION", "Dev") 16 | 17 | application = get_wsgi_application() 18 | -------------------------------------------------------------------------------- /docker/services/stock/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docker/services/stock/tests/__init__.py -------------------------------------------------------------------------------- /docs/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docs/architecture.png -------------------------------------------------------------------------------- /docs/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docs/flow.png -------------------------------------------------------------------------------- /docs/microservice_action.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/docs/microservice_action.gif -------------------------------------------------------------------------------- /docs/saga.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "0f0e716c-73ab-4500-a460-184ee51867e6", 4 | "name": "Saga with Outbox Pattern", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "46551" 7 | }, 8 | "item": [ 9 | { 10 | "name": "order/api/v1/orders/", 11 | "request": { 12 | "method": "POST", 13 | "header": [], 14 | "body": { 15 | "mode": "raw", 16 | "raw": "{\n \"customer_id\": 1,\n \"product_id\" : 1,\n \"amount\": 1,\n \"quantity\": 1\n}", 17 | "options": { 18 | "raw": { 19 | "language": "json" 20 | } 21 | } 22 | }, 23 | "url": { 24 | "raw": "http://127.0.0.1:8080/order/api/v1/orders/", 25 | "protocol": "http", 26 | "host": [ 27 | "127", 28 | "0", 29 | "0", 30 | "1" 31 | ], 32 | "port": "8080", 33 | "path": [ 34 | "order", 35 | "api", 36 | "v1", 37 | "orders", 38 | "" 39 | ] 40 | } 41 | }, 42 | "response": [] 43 | }, 44 | { 45 | "name": "order/api/v1/orders/", 46 | "protocolProfileBehavior": { 47 | "disableBodyPruning": true 48 | }, 49 | "request": { 50 | "method": "GET", 51 | "header": [], 52 | "body": { 53 | "mode": "raw", 54 | "raw": "", 55 | "options": { 56 | "raw": { 57 | "language": "json" 58 | } 59 | } 60 | }, 61 | "url": { 62 | "raw": "http://127.0.0.1:8080/order/api/v1/orders/", 63 | "protocol": "http", 64 | "host": [ 65 | "127", 66 | "0", 67 | "0", 68 | "1" 69 | ], 70 | "port": "8080", 71 | "path": [ 72 | "order", 73 | "api", 74 | "v1", 75 | "orders", 76 | "" 77 | ] 78 | } 79 | }, 80 | "response": [] 81 | }, 82 | { 83 | "name": "stock/api/v1/stocks/", 84 | "protocolProfileBehavior": { 85 | "disableBodyPruning": true 86 | }, 87 | "request": { 88 | "method": "GET", 89 | "header": [], 90 | "body": { 91 | "mode": "raw", 92 | "raw": "", 93 | "options": { 94 | "raw": { 95 | "language": "json" 96 | } 97 | } 98 | }, 99 | "url": { 100 | "raw": "http://127.0.0.1:8080/stock/api/v1/stocks/", 101 | "protocol": "http", 102 | "host": [ 103 | "127", 104 | "0", 105 | "0", 106 | "1" 107 | ], 108 | "port": "8080", 109 | "path": [ 110 | "stock", 111 | "api", 112 | "v1", 113 | "stocks", 114 | "" 115 | ] 116 | } 117 | }, 118 | "response": [] 119 | }, 120 | { 121 | "name": "stock/api/v1/reservations/", 122 | "protocolProfileBehavior": { 123 | "disableBodyPruning": true 124 | }, 125 | "request": { 126 | "method": "GET", 127 | "header": [], 128 | "body": { 129 | "mode": "raw", 130 | "raw": "", 131 | "options": { 132 | "raw": { 133 | "language": "json" 134 | } 135 | } 136 | }, 137 | "url": { 138 | "raw": "http://127.0.0.1:8080/stock/api/v1/reservations/", 139 | "protocol": "http", 140 | "host": [ 141 | "127", 142 | "0", 143 | "0", 144 | "1" 145 | ], 146 | "port": "8080", 147 | "path": [ 148 | "stock", 149 | "api", 150 | "v1", 151 | "reservations", 152 | "" 153 | ] 154 | } 155 | }, 156 | "response": [] 157 | }, 158 | { 159 | "name": "payment/api/v1/payments/", 160 | "protocolProfileBehavior": { 161 | "disableBodyPruning": true 162 | }, 163 | "request": { 164 | "method": "GET", 165 | "header": [], 166 | "body": { 167 | "mode": "raw", 168 | "raw": "", 169 | "options": { 170 | "raw": { 171 | "language": "json" 172 | } 173 | } 174 | }, 175 | "url": { 176 | "raw": "http://127.0.0.1:8080/payment/api/v1/payments/", 177 | "protocol": "http", 178 | "host": [ 179 | "127", 180 | "0", 181 | "0", 182 | "1" 183 | ], 184 | "port": "8080", 185 | "path": [ 186 | "payment", 187 | "api", 188 | "v1", 189 | "payments", 190 | "" 191 | ] 192 | } 193 | }, 194 | "response": [] 195 | } 196 | ] 197 | } -------------------------------------------------------------------------------- /k8s/k3d/README.md: -------------------------------------------------------------------------------- 1 | # k3d - Lightweight Kubernetes Distribution 🌟 2 | 3 | k3d is a tool designed to easily create, manage, and operate Kubernetes clusters. It is lightweight, efficient, and provides a seamless way to work with Kubernetes clusters for development and testing purposes. 4 | 5 | ## Installation 🚀 6 | 7 | ### Linux: 8 | 9 | ```bash 10 | curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash 11 | ``` 12 | 13 | ### macOS: 14 | 15 | ```bash 16 | brew install k3d 17 | ``` 18 | 19 | ### Windows: 20 | 21 | You can download the latest executable from the [official GitHub releases page](https://github.com/rancher/k3d/releases) and add it to your PATH. 22 | 23 | ## Creating a Cluster 🛠️ 24 | 25 | You can create a cluster with or without specifying the number of server nodes. Below are two methods: 26 | 27 | ### Method 1: Creating a Cluster with Server Nodes 28 | 29 | To create a cluster with server nodes, you can use the following command: 30 | 31 | ```bash 32 | k3d cluster create saga --servers 3 --agents 3 --port '8080:30000@loadbalancer' 33 | ``` 34 | 35 | This command creates a Kubernetes cluster named "saga" with port forwarding configured to forward traffic from port 8080 on the host to port 30000 on the load balancer, along with three server nodes and three agent nodes, providing a comprehensive environment for your Kubernetes development and testing needs. 36 | 37 | ### Method 2: Creating a Cluster without Agent Nodes 38 | 39 | If you don't need agent nodes and only want server nodes, you can create a cluster without specifying the number of agent nodes. Here's how: 40 | 41 | ```bash 42 | k3d cluster create saga --port '8080:30000@loadbalancer' 43 | ``` 44 | 45 | This command creates a Kubernetes cluster named "saga" with port forwarding configured to forward traffic from port 8080 on the host to port 30000 on the load balancer, along with three server nodes. This setup is suitable for scenarios where you only require server nodes for your Kubernetes environment. 46 | 47 | ### Deleting a Cluster 🗑️ 48 | 49 | To delete a cluster, use this command: 50 | 51 | ```bash 52 | k3d cluster delete saga 53 | ``` -------------------------------------------------------------------------------- /k8s/kong/README.md: -------------------------------------------------------------------------------- 1 | # Kong Ingress Controller Installation Guide 🛠️ 2 | 3 | This guide will walk you through the steps to install Kong Ingress Controller on your Kubernetes cluster. 4 | 5 | ### Prerequisites ✔️ 6 | 7 | - [Access to a Kubernetes cluster](../k3d/README.md) 8 | - `kubectl` configured to communicate with your cluster 9 | - [Helm installed](https://helm.sh/docs/intro/install/) 10 | 11 | ### Installation Steps 🚀 12 | 13 | ## Install the Gateway APIs 14 | 15 | 1. **Install the Gateway API CRDs before installing Kong Ingress Controller.** 16 | 17 | ```bash 18 | kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml 19 | ``` 20 | 21 | 2. **Create a Gateway and GatewayClass instance to use.** 22 | 23 | ```bash 24 | kubectl apply -f kong/kong-gateway.yaml 25 | ``` 26 | 27 | 3. **Helm Chart Installation** 28 | 29 | 1. Add the Kong Helm charts: 30 | ```bash 31 | helm repo add kong https://charts.konghq.com 32 | ``` 33 | 2. Update repo: 34 | ```bash 35 | helm repo update 36 | ``` 37 | 3. Install Kong Ingress Controller and Kong Gateway with Helm: 38 | ```bash 39 | helm install kong kong/ingress -n kong --create-namespace --values kong/values.yaml 40 | ``` 41 | 42 | 4. **Verify Installation** 43 | 44 | After installation, ensure that Kong Ingress Controller pods are running: 45 | 46 | ```bash 47 | curl -i 'localhost:8080' 48 | ``` 49 | 50 | The results should look like this: 51 | ```bash 52 | HTTP/1.1 404 Not Found 53 | Date: Sun, 28 Jan 2024 19:14:45 GMT 54 | Content-Type: application/json; charset=utf-8 55 | Connection: keep-alive 56 | Content-Length: 103 57 | X-Kong-Response-Latency: 0 58 | Server: kong/3.5.0 59 | X-Kong-Request-Id: fa55be13bee8575984a67514efbe224c 60 | 61 | { 62 | "message":"no Route matched with those values", 63 | "request_id":"fa55be13bee8575984a67514efbe224c" 64 | } 65 | ``` 66 | **Note:** 67 | 68 | If you encounter `curl: (52) Empty reply from server`, please wait a moment and try again. 69 | 70 | ### Uninstallation ❌ 71 | 72 | To uninstall Kong Ingress Controller, simply delete the Helm release: 73 | 74 | ```bash 75 | helm uninstall kong -n kong 76 | ``` -------------------------------------------------------------------------------- /k8s/kong/kong-gateway.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: gateway.networking.k8s.io/v1 3 | kind: GatewayClass 4 | metadata: 5 | name: kong 6 | annotations: 7 | konghq.com/gatewayclass-unmanaged: 'true' 8 | 9 | spec: 10 | controllerName: konghq.com/kic-gateway-controller 11 | 12 | --- 13 | apiVersion: gateway.networking.k8s.io/v1 14 | kind: Gateway 15 | metadata: 16 | name: kong 17 | spec: 18 | gatewayClassName: kong 19 | listeners: 20 | - name: proxy 21 | port: 80 22 | protocol: HTTP 23 | 24 | --- 25 | apiVersion: configuration.konghq.com/v1 26 | kind: KongClusterPlugin 27 | metadata: 28 | name: global-cors 29 | annotations: 30 | kubernetes.io/ingress.class: kong 31 | labels: 32 | global: "true" 33 | config: 34 | origins: 35 | - '*' 36 | methods: 37 | - GET 38 | - POST 39 | headers: 40 | - Accept 41 | - Accept-Version 42 | - Content-Length 43 | - Content-MD5 44 | - Content-Type 45 | - Date 46 | - X-Auth-Token 47 | exposed_headers: 48 | - X-Auth-Token 49 | credentials: true 50 | max_age: 3600 51 | plugin: cors -------------------------------------------------------------------------------- /k8s/kong/values.yaml: -------------------------------------------------------------------------------- 1 | gateway: 2 | proxy: 3 | type: NodePort 4 | http: 5 | nodePort: 30000 6 | 7 | -------------------------------------------------------------------------------- /k8s/rabbitmq/rabbitmq.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rabbitmq.com/v1beta1 2 | kind: RabbitmqCluster 3 | metadata: 4 | name: rabbitmq 5 | spec: 6 | replicas: 1 7 | rabbitmq: 8 | additionalConfig: | 9 | default_user=guest 10 | default_pass=guest 11 | additionalPlugins: 12 | - rabbitmq_management 13 | - rabbitmq_stomp 14 | - rabbitmq_shovel 15 | - rabbitmq_shovel_management 16 | -------------------------------------------------------------------------------- /k8s/saga/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /k8s/saga/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: saga 3 | description: A Helm chart for Kubernetes 4 | type: application 5 | version: 0.1.0 6 | appVersion: "0.1.0" 7 | -------------------------------------------------------------------------------- /k8s/saga/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{.Values.name}}-config-map 5 | data: 6 | DJANGO_DEBUG: "True" 7 | DJANGO_ALLOWED_HOSTS: "*" 8 | DJANGO_LANGUAGE_CODE: "en-us" 9 | DJANGO_TIME_ZONE: "UTC" 10 | DJANGO_USE_I18N: "True" 11 | DJANGO_USE_TZ: "True" 12 | DJANGO_CSRF_COOKIE_SECURE: "False" 13 | DJANGO_SESSION_COOKIE_SECURE: "False" 14 | DJANGO_CONFIGURATION: "Dev" -------------------------------------------------------------------------------- /k8s/saga/templates/deployments.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: {{.Values.name}}-deployment 6 | labels: 7 | app: {{.Values.name}} 8 | spec: 9 | replicas: {{.Values.replicas}} 10 | selector: 11 | matchLabels: 12 | app: {{.Values.name}} 13 | template: 14 | metadata: 15 | labels: 16 | app: {{.Values.name}} 17 | spec: 18 | containers: 19 | - name: {{.Values.name}}-container 20 | image: "{{.Values.image.repository}}:{{.Values.image.tag}}" 21 | imagePullPolicy: {{.Values.image.pullPolicy}} 22 | ports: 23 | - containerPort: {{.Values.port}} 24 | envFrom: 25 | - configMapRef: 26 | name: {{.Values.name}}-config-map 27 | - secretRef: 28 | name: {{.Values.name}}-secret 29 | resources: 30 | limits: 31 | cpu: "2" 32 | memory: "1024Mi" 33 | requests: 34 | cpu: "1" 35 | memory: "512Mi" 36 | initContainers: 37 | - name: migrations-container 38 | image: "{{.Values.image.repository}}:{{.Values.image.tag}}" 39 | command: [ "/bin/sh" ] 40 | args: 41 | - "-c" 42 | - python manage.py migrate 43 | envFrom: 44 | - configMapRef: 45 | name: {{.Values.name}}-config-map 46 | - secretRef: 47 | name: {{.Values.name}}-secret 48 | - name: loaddata-container 49 | image: "{{.Values.image.repository}}:{{.Values.image.tag}}" 50 | command: [ "/bin/sh" ] 51 | args: 52 | - "-c" 53 | - python manage.py loaddata {{.Values.name}} 54 | envFrom: 55 | - configMapRef: 56 | name: {{.Values.name}}-config-map 57 | - secretRef: 58 | name: {{.Values.name}}-secret 59 | restartPolicy: Always 60 | 61 | --- 62 | apiVersion: apps/v1 63 | kind: Deployment 64 | metadata: 65 | name: {{.Values.name}}-deployment-consumer 66 | labels: 67 | app: {{.Values.name}} 68 | spec: 69 | replicas: {{.Values.replicas}} 70 | selector: 71 | matchLabels: 72 | app: {{.Values.name}} 73 | template: 74 | metadata: 75 | labels: 76 | app: {{.Values.name}} 77 | spec: 78 | containers: 79 | - name: {{.Values.name}}-container 80 | image: "{{.Values.image.repository}}:{{.Values.image.tag}}" 81 | command: [ "python", "manage.py", "subscribe", "src.core.callback.callback", "/exchange/saga/{{.Values.name}}.v1", "{{.Values.name}}.v1" ] 82 | imagePullPolicy: {{.Values.image.pullPolicy}} 83 | envFrom: 84 | - configMapRef: 85 | name: {{.Values.name}}-config-map 86 | - secretRef: 87 | name: {{.Values.name}}-secret 88 | restartPolicy: Always 89 | --- 90 | apiVersion: apps/v1 91 | kind: Deployment 92 | metadata: 93 | name: {{.Values.name}}-deployment-producer 94 | labels: 95 | app: {{.Values.name}} 96 | spec: 97 | replicas: {{.Values.replicas}} 98 | selector: 99 | matchLabels: 100 | app: {{.Values.name}} 101 | template: 102 | metadata: 103 | labels: 104 | app: {{.Values.name}} 105 | spec: 106 | containers: 107 | - name: {{.Values.name}}-container 108 | image: "{{.Values.image.repository}}:{{.Values.image.tag}}" 109 | command: [ "python", "manage.py", "publish"] 110 | imagePullPolicy: {{.Values.image.pullPolicy}} 111 | envFrom: 112 | - configMapRef: 113 | name: {{.Values.name}}-config-map 114 | - secretRef: 115 | name: {{.Values.name}}-secret 116 | restartPolicy: Always 117 | --- 118 | apiVersion: apps/v1 119 | kind: Deployment 120 | metadata: 121 | name: {{.Values.name}}-db-deployment 122 | labels: 123 | app: {{.Values.name}}-db 124 | spec: 125 | replicas: 1 126 | selector: 127 | matchLabels: 128 | app: {{.Values.name}}-db 129 | template: 130 | metadata: 131 | labels: 132 | app: {{.Values.name}}-db 133 | spec: 134 | containers: 135 | - name: {{.Values.name}}-db-container 136 | image: postgres:15 137 | imagePullPolicy: IfNotPresent 138 | ports: 139 | - containerPort: 5432 140 | env: 141 | - name: POSTGRES_DB 142 | value: {{.Values.postgres.db | quote}} 143 | - name: POSTGRES_USER 144 | value: {{.Values.postgres.user | quote}} 145 | - name: POSTGRES_PASSWORD 146 | value: {{.Values.postgres.password | quote}} 147 | restartPolicy: Always -------------------------------------------------------------------------------- /k8s/saga/templates/httproute.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: gateway.networking.k8s.io/v1 2 | kind: HTTPRoute 3 | metadata: 4 | name: {{.Values.name}}-http-route 5 | annotations: 6 | konghq.com/strip-path: 'true' 7 | spec: 8 | parentRefs: 9 | - name: kong 10 | rules: 11 | - matches: 12 | - path: 13 | type: PathPrefix 14 | value: /{{.Values.name}} 15 | backendRefs: 16 | - name: {{.Values.name}}-service 17 | kind: Service 18 | port: {{.Values.port}} 19 | 20 | 21 | -------------------------------------------------------------------------------- /k8s/saga/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: {{.Values.name}}-secret 5 | type: Opaque 6 | data: 7 | DJANGO_SECRET_KEY: {{.Values.secret_key | b64enc | quote}} 8 | DATABASE_URL: {{.Values.database_url | b64enc | quote}} 9 | -------------------------------------------------------------------------------- /k8s/saga/templates/services.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: {{.Values.name}}-service 6 | labels: 7 | app: {{.Values.name}} 8 | spec: 9 | selector: 10 | app: {{.Values.name}} 11 | ports: 12 | - protocol: TCP 13 | port: {{.Values.port}} 14 | targetPort: {{.Values.port}} 15 | type: ClusterIP 16 | 17 | --- 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: {{.Values.name}}-db-service 22 | labels: 23 | app: {{.Values.name}}-db 24 | spec: 25 | selector: 26 | app: {{.Values.name}}-db 27 | ports: 28 | - protocol: TCP 29 | port: 5432 30 | targetPort: 5432 31 | type: ClusterIP -------------------------------------------------------------------------------- /k8s/saga/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for saga. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicas: 1 6 | 7 | image: 8 | repository: "" 9 | pullPolicy: IfNotPresent 10 | tag: "v1" 11 | 12 | name: "app" 13 | 14 | port: 8000 15 | 16 | # To simplify my examples. There are other means for the correct use of credentials. 17 | secret_key: "secret_key" 18 | 19 | postgres: 20 | db: "db" 21 | user: "user" 22 | password: "password" 23 | 24 | database_url: "postgres://user:password@db-service:5432/db" -------------------------------------------------------------------------------- /k8s/services/order/values.yaml: -------------------------------------------------------------------------------- 1 | replicas: 1 2 | 3 | image: 4 | repository: "hugobrilhante/order" 5 | pullPolicy: Always 6 | tag: "v1" 7 | 8 | name: "order" 9 | 10 | port: 8000 11 | 12 | secret_key: "secret_key" 13 | 14 | postgres: 15 | db: "db" 16 | user: "user" 17 | password: "password" 18 | 19 | database_url: "postgres://user:password@order-db-service:5432/db" -------------------------------------------------------------------------------- /k8s/services/payment/values.yaml: -------------------------------------------------------------------------------- 1 | replicas: 1 2 | 3 | image: 4 | repository: "hugobrilhante/payment" 5 | pullPolicy: Always 6 | tag: "v1" 7 | 8 | name: "payment" 9 | 10 | port: 8002 11 | 12 | secret_key: "secret_key" 13 | 14 | postgres: 15 | db: "db" 16 | user: "user" 17 | password: "password" 18 | 19 | database_url: "postgres://user:password@payment-db-service:5432/db" -------------------------------------------------------------------------------- /k8s/services/stock/values.yaml: -------------------------------------------------------------------------------- 1 | replicas: 1 2 | 3 | image: 4 | repository: "hugobrilhante/stock" 5 | pullPolicy: Always 6 | tag: "v1" 7 | 8 | name: "stock" 9 | 10 | port: 8001 11 | 12 | secret_key: "secret_key" 13 | 14 | postgres: 15 | db: "db" 16 | user: "user" 17 | password: "password" 18 | 19 | database_url: "postgres://user:password@stock-db-service:5432/db" -------------------------------------------------------------------------------- /k8s/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Function to install k3d 4 | install_k3d() { 5 | echo "🚀 Installing k3d..." 6 | # Install k3d based on the operating system 7 | if [[ "$(uname)" == "Darwin" ]]; then 8 | brew install k3d 9 | elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then 10 | curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash 11 | else 12 | echo "❌ Unsupported operating system" 13 | exit 1 14 | fi 15 | } 16 | 17 | # Function to create k3d cluster 18 | create_k3d_cluster() { 19 | echo "🌟 Creating k3d cluster..." 20 | k3d cluster create saga --port '8080:30000@loadbalancer' 21 | } 22 | 23 | # Function to install kubectl 24 | install_kubectl() { 25 | echo "🚀 Installing kubectl..." 26 | # Install kubectl 27 | if [[ "$(uname)" == "Darwin" ]]; then 28 | brew install kubectl 29 | elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then 30 | curl -LO https://dl.k8s.io/release/v1.23.2/bin/linux/amd64/kubectl 31 | sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl 32 | else 33 | echo "❌ Unsupported operating system" 34 | exit 1 35 | fi 36 | } 37 | 38 | # Function to install krew 39 | install_krew() { 40 | echo "🚀 Installing krew..." 41 | ( 42 | set -x; cd "$(mktemp -d)" && 43 | OS="$(uname | tr '[:upper:]' '[:lower:]')" && 44 | ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" && 45 | KREW="krew-${OS}_${ARCH}" && 46 | curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" && 47 | tar zxvf "${KREW}.tar.gz" && 48 | ./"${KREW}" install krew 49 | ) 50 | 51 | # Add krew to PATH in shell configuration 52 | if [[ -d "$HOME/.krew/bin" ]]; then 53 | if [[ ":$PATH:" != *":$HOME/.krew/bin:"* ]]; then 54 | echo 'export PATH="$HOME/.krew/bin:$PATH"' >> "$shell_config" 55 | export PATH="$HOME/.krew/bin:$PATH" 56 | fi 57 | fi 58 | } 59 | 60 | # Function to install RabbitMQ plugin for kubectl 61 | install_rabbitmq_plugin() { 62 | echo "🚀 Installing kubectl RabbitMQ plugin..." 63 | kubectl krew install rabbitmq 64 | } 65 | 66 | # Function to install Helm 67 | install_helm() { 68 | echo "🚀 Installing Helm..." 69 | # Install Helm 70 | if [[ "$(uname)" == "Darwin" ]]; then 71 | brew install helm 72 | elif [[ "$(expr substr $(uname -s) 1 5)" == "Linux" ]]; then 73 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 74 | chmod 700 get_helm.sh 75 | ./get_helm.sh 76 | else 77 | echo "❌ Unsupported operating system" 78 | exit 1 79 | fi 80 | } 81 | 82 | # Function to choose shell 83 | choose_shell() { 84 | PS3="Please select your preferred shell: " 85 | select option in "bash" "zsh" "quit"; do 86 | case "$option" in 87 | "bash") 88 | shell_config="$HOME/.bashrc" 89 | break 90 | ;; 91 | "zsh") 92 | shell_config="$HOME/.zshrc" 93 | break 94 | ;; 95 | "quit") 96 | exit 97 | ;; 98 | *) 99 | echo "Invalid option. Please select again." 100 | ;; 101 | esac 102 | done 103 | } 104 | 105 | # Main script 106 | 107 | # Install k3d if not installed 108 | if ! command -v k3d &> /dev/null; then 109 | install_k3d 110 | fi 111 | 112 | # Create k3d cluster 113 | create_k3d_cluster 114 | 115 | # Install kubectl if not installed 116 | if ! command -v kubectl &> /dev/null; then 117 | install_kubectl 118 | fi 119 | 120 | # Choose shell 121 | choose_shell 122 | 123 | # Install krew 124 | install_krew 125 | 126 | # Install RabbitMQ plugin for kubectl 127 | install_rabbitmq_plugin 128 | 129 | # Install Helm if not installed 130 | if ! command -v helm &> /dev/null; then 131 | install_helm 132 | fi 133 | 134 | echo "✅ Setup completed successfully!" 135 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /web/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "server": "node server.js" 11 | }, 12 | "dependencies": { 13 | "axios": "^1.6.7", 14 | "bootstrap": "^5.3.2", 15 | "express": "^4.18.2", 16 | "next": "14.1.0", 17 | "react": "^18", 18 | "react-bootstrap": "^2.10.0", 19 | "react-dom": "^18", 20 | "socket.io": "^4.7.4", 21 | "socket.io-client": "^4.7.4" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hugobrilhante/django-outbox-pattern-usage/c5713dfd4bb80714e3eca1f0072e527aaf668327/web/src/app/favicon.ico -------------------------------------------------------------------------------- /web/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", 5 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", 6 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /web/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | 4 | const inter = Inter({ subsets: ["latin"] }); 5 | 6 | export const metadata: Metadata = { 7 | title: "Saga Outbox Pattern", 8 | description: "Saga Outbox Pattern Status", 9 | }; 10 | 11 | export default function RootLayout({ 12 | children, 13 | }: Readonly<{ 14 | children: React.ReactNode; 15 | }>) { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /web/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | max-width: 100%; 46 | width: var(--max-width); 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | text-wrap: balance; 74 | } 75 | 76 | .center { 77 | display: flex; 78 | justify-content: center; 79 | align-items: center; 80 | position: relative; 81 | padding: 4rem 0; 82 | } 83 | 84 | .center::before { 85 | background: var(--secondary-glow); 86 | border-radius: 50%; 87 | width: 480px; 88 | height: 360px; 89 | margin-left: -400px; 90 | } 91 | 92 | .center::after { 93 | background: var(--primary-glow); 94 | width: 240px; 95 | height: 180px; 96 | z-index: -1; 97 | } 98 | 99 | .center::before, 100 | .center::after { 101 | content: ""; 102 | left: 50%; 103 | position: absolute; 104 | filter: blur(45px); 105 | transform: translateZ(0); 106 | } 107 | 108 | .logo { 109 | position: relative; 110 | } 111 | /* Enable hover only on non-touch devices */ 112 | @media (hover: hover) and (pointer: fine) { 113 | .card:hover { 114 | background: rgba(var(--card-rgb), 0.1); 115 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 116 | } 117 | 118 | .card:hover span { 119 | transform: translateX(4px); 120 | } 121 | } 122 | 123 | @media (prefers-reduced-motion) { 124 | .card:hover span { 125 | transform: none; 126 | } 127 | } 128 | 129 | /* Mobile */ 130 | @media (max-width: 700px) { 131 | .content { 132 | padding: 4rem; 133 | } 134 | 135 | .grid { 136 | grid-template-columns: 1fr; 137 | margin-bottom: 120px; 138 | max-width: 320px; 139 | text-align: center; 140 | } 141 | 142 | .card { 143 | padding: 1rem 2.5rem; 144 | } 145 | 146 | .card h2 { 147 | margin-bottom: 0.5rem; 148 | } 149 | 150 | .center { 151 | padding: 8rem 0 6rem; 152 | } 153 | 154 | .center::before { 155 | transform: none; 156 | height: 300px; 157 | } 158 | 159 | .description { 160 | font-size: 0.8rem; 161 | } 162 | 163 | .description a { 164 | padding: 1rem; 165 | } 166 | 167 | .description p, 168 | .description div { 169 | display: flex; 170 | justify-content: center; 171 | position: fixed; 172 | width: 100%; 173 | } 174 | 175 | .description p { 176 | align-items: center; 177 | inset: 0 0 auto; 178 | padding: 2rem 1rem 1.4rem; 179 | border-radius: 0; 180 | border: none; 181 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 182 | background: linear-gradient( 183 | to bottom, 184 | rgba(var(--background-start-rgb), 1), 185 | rgba(var(--callout-rgb), 0.5) 186 | ); 187 | background-clip: padding-box; 188 | backdrop-filter: blur(24px); 189 | } 190 | 191 | .description div { 192 | align-items: flex-end; 193 | pointer-events: none; 194 | inset: auto 0 0; 195 | padding: 2rem; 196 | height: 200px; 197 | background: linear-gradient( 198 | to bottom, 199 | transparent 0%, 200 | rgb(var(--background-end-rgb)) 40% 201 | ); 202 | z-index: 1; 203 | } 204 | } 205 | 206 | /* Tablet and Smaller Desktop */ 207 | @media (min-width: 701px) and (max-width: 1120px) { 208 | .grid { 209 | grid-template-columns: repeat(2, 50%); 210 | } 211 | } 212 | 213 | @media (prefers-color-scheme: dark) { 214 | .vercelLogo { 215 | filter: invert(1); 216 | } 217 | 218 | .logo { 219 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 220 | } 221 | } 222 | 223 | @keyframes rotate { 224 | from { 225 | transform: rotate(360deg); 226 | } 227 | to { 228 | transform: rotate(0deg); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /web/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import "bootstrap/dist/css/bootstrap.min.css"; 3 | import React, { useEffect, useState } from 'react'; 4 | import { Badge, Container, Table, Button, Form } from 'react-bootstrap'; 5 | import axios from 'axios'; 6 | 7 | interface Order { 8 | order_id: string; 9 | status: string; 10 | } 11 | 12 | interface Stock { 13 | status: string; 14 | } 15 | 16 | interface Payment { 17 | status: string; 18 | } 19 | 20 | interface Status { 21 | status: string; 22 | } 23 | 24 | const Home: React.FC = () => { 25 | const [orders, setOrders] = useState([]); 26 | const [stocks, setStocks] = useState([]); 27 | const [payments, setPayments] = useState([]); 28 | 29 | useEffect(() => { 30 | const fetchData = async () => { 31 | try { 32 | const orderResponse = await axios.get('http://localhost:8080/order/api/v1/orders/'); 33 | setOrders(orderResponse.data); 34 | 35 | const stockResponse = await axios.get('http://localhost:8080/stock/api/v1/reservations/'); 36 | setStocks(stockResponse.data); 37 | 38 | const paymentResponse = await axios.get('http://localhost:8080/payment/api/v1/payments/'); 39 | setPayments(paymentResponse.data); 40 | } catch (error) { 41 | console.error('Error fetching data:', error); 42 | } 43 | }; 44 | 45 | const interval = setInterval(fetchData, 500); 46 | 47 | return () => clearInterval(interval); 48 | }, []); 49 | 50 | const getStatusVariant = (status: string): string => { 51 | switch (status) { 52 | case 'payment_confirmed': 53 | return 'success'; 54 | case 'reserved': 55 | return 'primary'; 56 | case 'payment_denied': 57 | return 'danger'; 58 | case 'not_reserved': 59 | return 'warning'; 60 | case 'created': 61 | return 'secondary'; 62 | case 'payment not touch': 63 | return 'warning'; 64 | default: 65 | return 'info'; 66 | } 67 | }; 68 | 69 | const findByID = (list: any[], order_id: string): Status => { 70 | const found = list.find(item => item.order_id == order_id); 71 | return found ? found : { status: "payment not touch" }; 72 | } 73 | 74 | const handleClick = async (amount: number, quantity: number) => { 75 | try { 76 | const response = await axios.post('http://localhost:8080/order/api/v1/orders/', { 77 | order_id: 1, 78 | customer_id: "1", 79 | product_id: "1", 80 | amount: amount, 81 | quantity: quantity 82 | }); 83 | console.log('API Response:', response.data); 84 | } catch (error) { 85 | console.error('Error making API request:', error); 86 | } 87 | }; 88 | 89 | const handlePaymentConfirmedButtonClick = () => { 90 | handleClick(1, 1); 91 | }; 92 | 93 | const handlePaymentDeniedButtonClick = () => { 94 | handleClick(1000, 1); 95 | }; 96 | 97 | const handleOutOfStockButtonClick = () => { 98 | handleClick(1, 11); 99 | }; 100 | 101 | return ( 102 | 103 |

Microservice Actions

104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | {orders.map((order, index) => { 114 | let order_status = order.status === 'created' ? "Loading..." : findByID(stocks, order.order_id).status 115 | let payment_status = order.status === 'created' ? "Loading..." : findByID(payments, order.order_id).status 116 | return ( 117 | 118 | 119 | 120 | 121 | 122 | ) 123 | })} 124 | 125 |
OrderStockPayment
{order.status}{order_status}{payment_status}
126 |
127 | 128 | 129 | 130 |
131 |
132 | ); 133 | }; 134 | 135 | export default Home; 136 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------