Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions certified_builder/certificates_on_solana.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class CertificatesOnSolana:

@staticmethod
def register_certificate_on_solana(certificate_data: dict) -> dict:
logger.info("Registering certificate on Solana blockchain")
logger.info("Registering certificate on Solana blockchain with data: %s", certificate_data)
"""
Registers a certificate on the Solana blockchain.

Expand All @@ -41,11 +41,10 @@ def register_certificate_on_solana(certificate_data: dict) -> dict:
},
json=certificate_data
)
logger.info(f"Solana response status code: {response.status_code}")
logger.info(f"Solana response status code: {response.status_code}")
response.raise_for_status()
solana_response = response.json()
return solana_response
logger.info("Certificate registered successfully on Solana")
except Exception as e:
logger.error(f"Error registering certificate on Solana: {str(e)}")
raise CertificatesOnSolanaException(details=str(e), cause=e)
20 changes: 12 additions & 8 deletions certified_builder/certified_builder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import logging
import tempfile
import os
from typing import List
from PIL import Image, ImageDraw, ImageFont
from io import BytesIO
from models.participant import Participant
from config import config
from certified_builder.utils.fetch_file_certificate import fetch_file_certificate
from certified_builder.certificates_on_solana import CertificatesOnSolana
from certified_builder.make_qrcode import MakeQRCode
Expand Down Expand Up @@ -33,6 +32,7 @@ def build_certificates(self, participants: List[Participant]):
# Cache for background and logo if they are the same for all participants
certificate_template = None
logo = None
logo_tech_floripa = None

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

if not logo_tech_floripa:
logo_tech_floripa = self._download_image(config.TECH_FLORIPA_LOGO_URL)

for participant in participants:
try:
# Register certificate on Solana, with returned data extract url for verification
Expand All @@ -57,21 +60,21 @@ def build_certificates(self, participants: List[Participant]):
"certificate_code": participant.formated_validation_code()
}
)

# alteração: agora usamos a função renomeada que apenas extrai o explorer_url
participant.authenticity_verification_url = extract_solana_explorer_url(solana_response=solana_response)

if not participant.authenticity_verification_url:
raise RuntimeError("Failed to get authenticity verification URL from Solana response")

logger.info(f"URL de verificação de autenticidade: {participant.authenticity_verification_url}")
# Download template and logo only if they are not shared
if not all_same_background:
certificate_template = self._download_image(participant.certificate.background)
if not all_same_logo:
logo = self._download_image(participant.certificate.logo)

# Generate and save certificate
certificate_generated = self.generate_certificate(participant, certificate_template, logo)
certificate_generated = self.generate_certificate(participant, certificate_template, logo, logo_tech_floripa)
certificate_path = self.save_certificate(certificate_generated, participant)

results.append({
Expand Down Expand Up @@ -121,7 +124,7 @@ def _ensure_valid_rgba(self, img: Image) -> Image:
new_img.paste(img.convert('RGB'), (0, 0))
return new_img

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


url_qr_code = f"{config.TECH_FLORIPA_CERTIFICATE_VALIDATE_URL}?validate_code={participant.formated_validation_code()}"
qrcode_size = (150, 150)
qr_code_image_io = MakeQRCode.generate_qr_code(participant.authenticity_verification_url)
logger.info(f"URL do QR code: {url_qr_code} para o certificado de {participant.name_completed()}")
qr_code_image_io = MakeQRCode.generate_qr_code(url_qr_code, logo_tech_floripa=logo_tech_floripa)
qr_code_image = Image.open(qr_code_image_io).convert("RGBA")
# comentário: para manter o QR nítido, usamos NEAREST ao redimensionar
if qr_code_image.size != qrcode_size:
Expand Down
26 changes: 22 additions & 4 deletions certified_builder/make_qrcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
import qrcode
import logging
from io import BytesIO
from PIL import Image
from qrcode.image.pil import PilImage

logger = logging.getLogger(__name__)

class MakeQRCode:
@staticmethod
def generate_qr_code(data: str) -> BytesIO:
def generate_qr_code(data: str, logo_tech_floripa: Image) -> BytesIO:
try:
logger.info("Generating QR code ")
logger.info(f"Generating QR code for {data}")
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
error_correction=qrcode.constants.ERROR_CORRECT_H,
box_size=10,
border=4,
)
Expand All @@ -22,10 +23,27 @@ def generate_qr_code(data: str) -> BytesIO:
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="transparent", image_factory=PilImage)
img = img.convert("RGBA")

# Redimensiona o logo para aproximadamente 30% do tamanho do QR code
# Tamanho limitado para não interferir nos padrões de detecção nos cantos
qr_width, qr_height = img.size
logo_size = int(min(qr_width, qr_height) * 0.3)
logo_resized = logo_tech_floripa.copy()
logo_resized.thumbnail((logo_size, logo_size), Image.Resampling.LANCZOS)
logo_resized = logo_resized.convert("RGBA")

# Calcula a posição central para colar o logo
logo_width, logo_height = logo_resized.size
position = ((qr_width - logo_width) // 2, (qr_height - logo_height) // 2)

# Cola o logo no centro do QR code mantendo transparência
# Com ERROR_CORRECT_H (30% redundância), o QR code permanece legível mesmo com o logo
img.paste(logo_resized, position, logo_resized)

byte_io = BytesIO()
img.save(byte_io, format='PNG')
byte_io.seek(0)
logger.info("QR code generated successfully")
logger.info(f"QR code generated successfully for {data}")
return byte_io
except Exception as e:
logging.error(f"Failed to generate QR code: {e}")
Expand Down
2 changes: 1 addition & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class Config(BaseSettings):
SERVICE_URL_REGISTRATION_API_SOLANA: str
SERVICE_API_KEY_REGISTRATION_API_SOLANA: str
TECH_FLORIPA_CERTIFICATE_VALIDATE_URL: str

TECH_FLORIPA_LOGO_URL: str
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
Expand Down
60 changes: 55 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Certified Builder Py

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.
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.

[![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)

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

## Estrutura do Projeto

```plaintext
project_root/
├── certified_builder/
│ ├── certified_builder.py # Classe principal de geração de certificados
│ ├── make_qrcode.py # Geração de QR codes com logo
│ ├── certificates_on_solana.py # Integração com blockchain Solana
│ ├── solana_explorer_url.py # Extração de URL do Solana Explorer
│ └── utils/
│ └── fetch_file_certificate.py # Utilitário para download de imagens
├── models/
│ ├── participant.py # Modelo de dados do participante
│ ├── certificate.py # Modelo de dados do certificado
│ └── event.py # Modelo de dados do evento
├── aws/
│ ├── sqs_service.py # Serviço para envio de mensagens SQS
│ ├── s3_service.py # Serviço para upload no S3
│ └── boto_aws.py # Configuração do cliente AWS
├── fonts/
│ ├── PinyonScript/ # Fonte para o nome do participante
│ └── ChakraPetch/ # Fonte para detalhes e código de validação
├── tests/ # Testes automatizados
├── lambda_function.py # Handler da função Lambda
├── config.py # Configurações do projeto
├── Dockerfile # Configuração do container
└── requirements.txt # Dependências do projeto
```
Expand All @@ -45,12 +56,16 @@ project_root/
- Pillow (Processamento de imagens)
- httpx (Requisições HTTP)
- Pydantic (Validação de dados)
- qrcode (Geração de QR codes)
- Docker
- AWS Lambda
- AWS ECR
- AWS SQS
- Solana Blockchain (Registro de certificados)

## Formato da Mensagem SQS
## Formato da Mensagem SQS (Entrada)

A Lambda recebe mensagens do SQS com os dados dos participantes para gerar os certificados:

```json
{
Expand All @@ -76,6 +91,38 @@ project_root/
}
```

## Formato da Mensagem SQS (Saída - Fila de Notificação)

Após a geração dos certificados, uma mensagem é enviada para outra fila SQS com os dados do certificado gerado:

```json
[
{
"order_id": 123,
"validation_code": "ABC-DEF-GHI",
"authenticity_verification_url": "https://explorer.solana.com/tx/...?cluster=devnet",
"product_id": 456,
"product_name": "Nome do Evento",
"email": "email@exemplo.com",
"certificate_key": "certificates/456/123/Nome_Sobrenome_Nome_do_Evento_ABC-DEF-GHI.png",
"success": true
}
]
```

### Campos da Mensagem de Saída

- **`order_id`**: ID do pedido/ordem
- **`validation_code`**: Código de validação do certificado (formato: XXX-XXX-XXX)
- **`authenticity_verification_url`**: URL do Solana Explorer para verificação na blockchain
- **`product_id`**: ID do produto/evento
- **`product_name`**: Nome do produto/evento
- **`email`**: Email do participante
- **`certificate_key`**: Chave do certificado no S3 (formato: `certificates/{product_id}/{order_id}/{nome_certificado}.png`)
- **`success`**: Indica se a geração foi bem-sucedida (true/false)

**Nota**: A mensagem é enviada como um array, podendo conter múltiplos certificados quando processados em lote.

## Desenvolvimento Local

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

## Estrutura do Certificado Gerado

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

## Contribuindo

Expand Down
3 changes: 2 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class MockConfig:
SERVICE_API_KEY_REGISTRATION_API_SOLANA = "test-api-key"
# comentário: URL de validação mockada para o Tech Floripa usada nos testes
TECH_FLORIPA_CERTIFICATE_VALIDATE_URL = "https://example.test/certificate-validate/"

TECH_FLORIPA_LOGO_URL = "https://example.test/logo.png"

# comentário: expõe tanto a classe quanto a instância, como o módulo real faria
mock_module.Config = MockConfig
mock_module.config = MockConfig()
Expand Down
Loading