Skip to content

Commit 8d2a8d7

Browse files
Merge pull request #15 from PythonFloripa/dev
Dev
2 parents f5544e1 + f8eb34d commit 8d2a8d7

File tree

8 files changed

+370
-34
lines changed

8 files changed

+370
-34
lines changed

certified_builder/certificates_on_solana.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class CertificatesOnSolana:
2121

2222
@staticmethod
2323
def register_certificate_on_solana(certificate_data: dict) -> dict:
24-
logger.info("Registering certificate on Solana blockchain")
24+
logger.info("Registering certificate on Solana blockchain with data: %s", certificate_data)
2525
"""
2626
Registers a certificate on the Solana blockchain.
2727
@@ -41,11 +41,10 @@ def register_certificate_on_solana(certificate_data: dict) -> dict:
4141
},
4242
json=certificate_data
4343
)
44-
logger.info(f"Solana response status code: {response.status_code}")
44+
logger.info(f"Solana response status code: {response.status_code}")
4545
response.raise_for_status()
4646
solana_response = response.json()
4747
return solana_response
48-
logger.info("Certificate registered successfully on Solana")
4948
except Exception as e:
5049
logger.error(f"Error registering certificate on Solana: {str(e)}")
5150
raise CertificatesOnSolanaException(details=str(e), cause=e)

certified_builder/certified_builder.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import logging
2-
import tempfile
32
import os
43
from typing import List
54
from PIL import Image, ImageDraw, ImageFont
6-
from io import BytesIO
75
from models.participant import Participant
6+
from config import config
87
from certified_builder.utils.fetch_file_certificate import fetch_file_certificate
98
from certified_builder.certificates_on_solana import CertificatesOnSolana
109
from certified_builder.make_qrcode import MakeQRCode
@@ -33,6 +32,7 @@ def build_certificates(self, participants: List[Participant]):
3332
# Cache for background and logo if they are the same for all participants
3433
certificate_template = None
3534
logo = None
35+
logo_tech_floripa = None
3636

3737
# Check if all participants share the same background and logo
3838
if participants:
@@ -46,6 +46,9 @@ def build_certificates(self, participants: List[Participant]):
4646
if all_same_logo:
4747
logo = self._download_image(first_participant.certificate.logo)
4848

49+
if not logo_tech_floripa:
50+
logo_tech_floripa = self._download_image(config.TECH_FLORIPA_LOGO_URL)
51+
4952
for participant in participants:
5053
try:
5154
# Register certificate on Solana, with returned data extract url for verification
@@ -57,21 +60,21 @@ def build_certificates(self, participants: List[Participant]):
5760
"certificate_code": participant.formated_validation_code()
5861
}
5962
)
60-
63+
6164
# alteração: agora usamos a função renomeada que apenas extrai o explorer_url
6265
participant.authenticity_verification_url = extract_solana_explorer_url(solana_response=solana_response)
6366

6467
if not participant.authenticity_verification_url:
6568
raise RuntimeError("Failed to get authenticity verification URL from Solana response")
66-
69+
logger.info(f"URL de verificação de autenticidade: {participant.authenticity_verification_url}")
6770
# Download template and logo only if they are not shared
6871
if not all_same_background:
6972
certificate_template = self._download_image(participant.certificate.background)
7073
if not all_same_logo:
7174
logo = self._download_image(participant.certificate.logo)
7275

7376
# Generate and save certificate
74-
certificate_generated = self.generate_certificate(participant, certificate_template, logo)
77+
certificate_generated = self.generate_certificate(participant, certificate_template, logo, logo_tech_floripa)
7578
certificate_path = self.save_certificate(certificate_generated, participant)
7679

7780
results.append({
@@ -121,7 +124,7 @@ def _ensure_valid_rgba(self, img: Image) -> Image:
121124
new_img.paste(img.convert('RGB'), (0, 0))
122125
return new_img
123126

124-
def generate_certificate(self, participant: Participant, certificate_template: Image, logo: Image):
127+
def generate_certificate(self, participant: Participant, certificate_template: Image, logo: Image, logo_tech_floripa: Image):
125128
"""Generate a certificate for a participant."""
126129
try:
127130
# Ensure images have valid transparency channels
@@ -145,9 +148,10 @@ def generate_certificate(self, participant: Participant, certificate_template: I
145148
# Fallback without using the logo as its own mask
146149
overlay.paste(logo, (50, 50))
147150

148-
151+
url_qr_code = f"{config.TECH_FLORIPA_CERTIFICATE_VALIDATE_URL}?validate_code={participant.formated_validation_code()}"
149152
qrcode_size = (150, 150)
150-
qr_code_image_io = MakeQRCode.generate_qr_code(participant.authenticity_verification_url)
153+
logger.info(f"URL do QR code: {url_qr_code} para o certificado de {participant.name_completed()}")
154+
qr_code_image_io = MakeQRCode.generate_qr_code(url_qr_code, logo_tech_floripa=logo_tech_floripa)
151155
qr_code_image = Image.open(qr_code_image_io).convert("RGBA")
152156
# comentário: para manter o QR nítido, usamos NEAREST ao redimensionar
153157
if qr_code_image.size != qrcode_size:

certified_builder/make_qrcode.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,19 @@
22
import qrcode
33
import logging
44
from io import BytesIO
5+
from PIL import Image
56
from qrcode.image.pil import PilImage
67

78
logger = logging.getLogger(__name__)
89

910
class MakeQRCode:
1011
@staticmethod
11-
def generate_qr_code(data: str) -> BytesIO:
12+
def generate_qr_code(data: str, logo_tech_floripa: Image) -> BytesIO:
1213
try:
13-
logger.info("Generating QR code ")
14+
logger.info(f"Generating QR code for {data}")
1415
qr = qrcode.QRCode(
1516
version=1,
16-
error_correction=qrcode.constants.ERROR_CORRECT_L,
17+
error_correction=qrcode.constants.ERROR_CORRECT_H,
1718
box_size=10,
1819
border=4,
1920
)
@@ -22,10 +23,27 @@ def generate_qr_code(data: str) -> BytesIO:
2223
qr.make(fit=True)
2324
img = qr.make_image(fill_color="black", back_color="transparent", image_factory=PilImage)
2425
img = img.convert("RGBA")
26+
27+
# Redimensiona o logo para aproximadamente 30% do tamanho do QR code
28+
# Tamanho limitado para não interferir nos padrões de detecção nos cantos
29+
qr_width, qr_height = img.size
30+
logo_size = int(min(qr_width, qr_height) * 0.3)
31+
logo_resized = logo_tech_floripa.copy()
32+
logo_resized.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS)
33+
logo_resized = logo_resized.convert("RGBA")
34+
35+
# Calcula a posição central para colar o logo
36+
logo_width, logo_height = logo_resized.size
37+
position = ((qr_width - logo_width) // 2, (qr_height - logo_height) // 2)
38+
39+
# Cola o logo no centro do QR code mantendo transparência
40+
# Com ERROR_CORRECT_H (30% redundância), o QR code permanece legível mesmo com o logo
41+
img.paste(logo_resized, position, logo_resized)
42+
2543
byte_io = BytesIO()
2644
img.save(byte_io, format='PNG')
2745
byte_io.seek(0)
28-
logger.info("QR code generated successfully")
46+
logger.info(f"QR code generated successfully for {data}")
2947
return byte_io
3048
except Exception as e:
3149
logging.error(f"Failed to generate QR code: {e}")

config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Config(BaseSettings):
1010
SERVICE_URL_REGISTRATION_API_SOLANA: str
1111
SERVICE_API_KEY_REGISTRATION_API_SOLANA: str
1212
TECH_FLORIPA_CERTIFICATE_VALIDATE_URL: str
13-
13+
TECH_FLORIPA_LOGO_URL: str
1414
class Config:
1515
env_file = ".env"
1616
env_file_encoding = "utf-8"

readme.md

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Certified Builder Py
22

3-
Sistema de geração automática de certificados para eventos usando AWS Lambda e Docker. O projeto gera certificados personalizados para participantes de eventos, processando mensagens do SQS e utilizando templates predefinidos.
3+
Sistema de geração automática de certificados para eventos usando AWS Lambda e Docker. O projeto gera certificados personalizados para participantes de eventos, processando mensagens do SQS e utilizando templates predefinidos. Os certificados incluem QR code para validação com logo Tech Floripa no centro e são registrados na blockchain Solana para autenticação.
44

55
[![Continuos Integration -Testing - Certified Builder Py](https://github.com/maxsonferovante/certified_builder_py/actions/workflows/workflow_testing.yaml/badge.svg)](https://github.com/maxsonferovante/certified_builder_py/actions/workflows/workflow_testing.yaml)
66

@@ -13,28 +13,39 @@ Sistema de geração automática de certificados para eventos usando AWS Lambda
1313
- Código de validação único
1414
- Logo do evento
1515
- Detalhes do evento em três linhas centralizadas
16-
- QR Code para validação
16+
- QR Code para validação com logo Tech Floripa no centro
17+
- Registro na blockchain Solana para autenticação
1718
- Processamento de mensagens SQS
1819
- Execução em container Docker
1920
- Deploy automatizado para AWS Lambda
2021
- Integração com AWS ECR
22+
- Envio de mensagens para fila de notificação com dados do certificado
2123

2224
## Estrutura do Projeto
2325

2426
```plaintext
2527
project_root/
2628
├── certified_builder/
2729
│ ├── certified_builder.py # Classe principal de geração de certificados
30+
│ ├── make_qrcode.py # Geração de QR codes com logo
31+
│ ├── certificates_on_solana.py # Integração com blockchain Solana
32+
│ ├── solana_explorer_url.py # Extração de URL do Solana Explorer
2833
│ └── utils/
2934
│ └── fetch_file_certificate.py # Utilitário para download de imagens
3035
├── models/
3136
│ ├── participant.py # Modelo de dados do participante
3237
│ ├── certificate.py # Modelo de dados do certificado
3338
│ └── event.py # Modelo de dados do evento
39+
├── aws/
40+
│ ├── sqs_service.py # Serviço para envio de mensagens SQS
41+
│ ├── s3_service.py # Serviço para upload no S3
42+
│ └── boto_aws.py # Configuração do cliente AWS
3443
├── fonts/
3544
│ ├── PinyonScript/ # Fonte para o nome do participante
3645
│ └── ChakraPetch/ # Fonte para detalhes e código de validação
46+
├── tests/ # Testes automatizados
3747
├── lambda_function.py # Handler da função Lambda
48+
├── config.py # Configurações do projeto
3849
├── Dockerfile # Configuração do container
3950
└── requirements.txt # Dependências do projeto
4051
```
@@ -45,12 +56,16 @@ project_root/
4556
- Pillow (Processamento de imagens)
4657
- httpx (Requisições HTTP)
4758
- Pydantic (Validação de dados)
59+
- qrcode (Geração de QR codes)
4860
- Docker
4961
- AWS Lambda
5062
- AWS ECR
5163
- AWS SQS
64+
- Solana Blockchain (Registro de certificados)
5265

53-
## Formato da Mensagem SQS
66+
## Formato da Mensagem SQS (Entrada)
67+
68+
A Lambda recebe mensagens do SQS com os dados dos participantes para gerar os certificados:
5469

5570
```json
5671
{
@@ -76,6 +91,38 @@ project_root/
7691
}
7792
```
7893

94+
## Formato da Mensagem SQS (Saída - Fila de Notificação)
95+
96+
Após a geração dos certificados, uma mensagem é enviada para outra fila SQS com os dados do certificado gerado:
97+
98+
```json
99+
[
100+
{
101+
"order_id": 123,
102+
"validation_code": "ABC-DEF-GHI",
103+
"authenticity_verification_url": "https://explorer.solana.com/tx/...?cluster=devnet",
104+
"product_id": 456,
105+
"product_name": "Nome do Evento",
106+
"email": "[email protected]",
107+
"certificate_key": "certificates/456/123/Nome_Sobrenome_Nome_do_Evento_ABC-DEF-GHI.png",
108+
"success": true
109+
}
110+
]
111+
```
112+
113+
### Campos da Mensagem de Saída
114+
115+
- **`order_id`**: ID do pedido/ordem
116+
- **`validation_code`**: Código de validação do certificado (formato: XXX-XXX-XXX)
117+
- **`authenticity_verification_url`**: URL do Solana Explorer para verificação na blockchain
118+
- **`product_id`**: ID do produto/evento
119+
- **`product_name`**: Nome do produto/evento
120+
- **`email`**: Email do participante
121+
- **`certificate_key`**: Chave do certificado no S3 (formato: `certificates/{product_id}/{order_id}/{nome_certificado}.png`)
122+
- **`success`**: Indica se a geração foi bem-sucedida (true/false)
123+
124+
**Nota**: A mensagem é enviada como um array, podendo conter múltiplos certificados quando processados em lote.
125+
79126
## Desenvolvimento Local
80127

81128
### Pré-requisitos
@@ -118,11 +165,14 @@ O deploy é automatizado através do GitHub Actions:
118165

119166
## Estrutura do Certificado Gerado
120167

121-
- **Logo**: Canto superior esquerdo (150x150 pixels)
168+
- **Logo do Evento**: Canto superior esquerdo (150x150 pixels máximo, redimensionado automaticamente)
122169
- **Nome**: Centro do certificado (fonte Pinyon Script)
123170
- **Detalhes**: Três linhas centralizadas abaixo do nome (fonte Chakra Petch)
171+
- **QR Code**: Posicionado abaixo do logo (150x150 pixels) com logo Tech Floripa centralizado
172+
- Contém URL de validação com código único: `https://tech.floripa.br/certificate-validate/?validate_code=XXX-XXX-XXX`
173+
- Usa correção de erros nível H (30% redundância) para garantir leitura mesmo com logo
174+
- **Texto "Scan to Validate"**: Abaixo do QR code, centralizado
124175
- **Código de Validação**: Canto inferior direito (fonte Chakra Petch)
125-
- **QR Code**: Canto inferior direito para validação online
126176

127177
## Contribuindo
128178

tests/conftest.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ class MockConfig:
2222
SERVICE_API_KEY_REGISTRATION_API_SOLANA = "test-api-key"
2323
# comentário: URL de validação mockada para o Tech Floripa usada nos testes
2424
TECH_FLORIPA_CERTIFICATE_VALIDATE_URL = "https://example.test/certificate-validate/"
25-
25+
TECH_FLORIPA_LOGO_URL = "https://example.test/logo.png"
26+
2627
# comentário: expõe tanto a classe quanto a instância, como o módulo real faria
2728
mock_module.Config = MockConfig
2829
mock_module.config = MockConfig()

0 commit comments

Comments
 (0)