Skip to content

Commit c63c7b2

Browse files
Add download certificate functionality: Implement DownloadCertificate class and related DTOs for handling certificate download requests. Introduce FileManager for S3 operations, update dependency container, and enhance API Gateway with a new download_certificate endpoint. Add HTML templates for download success, not found, and error responses.
1 parent ac3b343 commit c63c7b2

21 files changed

+428
-13
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import logging
2+
import uuid
3+
from src.domain.repository.certificate_repository import CertificateRepository
4+
from src.application.dto.download_certificate_dto import DownloadCertificateRequestDto, DownloadCertificateResponseDto
5+
from src.infrastructure.container.dependency_container import container
6+
from src.infrastructure.aws.file_manager import FileManager
7+
from src.domain.exception.certificate_not_found import CertificateNotFound
8+
9+
logger = logging.getLogger(__name__)
10+
11+
class DownloadCertificate:
12+
def __init__(self):
13+
self.certificate_repository: CertificateRepository = container.get('certificate_repository')
14+
self.file_manager: FileManager = container.get('file_manager')
15+
16+
def execute(self, request: DownloadCertificateRequestDto) -> DownloadCertificateResponseDto:
17+
logger.info(f"Buscando certificado para download com ID: {request.id}")
18+
19+
certificate = self.certificate_repository.find_by_id(request.id)
20+
21+
if not certificate:
22+
raise CertificateNotFound(f"Certificado não encontrado para ID: {request.id}")
23+
24+
certificate_url = ""
25+
26+
if certificate.success and certificate.certificate_key:
27+
certificate_url = self.file_manager.get_certificate_download_url(certificate.certificate_key)
28+
logger.info(f"URL pré-assinada gerada para certificado {request.id}")
29+
elif certificate.success and not certificate.certificate_key:
30+
logger.warning(f"Certificado {request.id} marcado como sucesso mas sem certificate_key")
31+
elif not certificate.success:
32+
logger.info(f"Certificado {request.id} não foi processado com sucesso")
33+
34+
return DownloadCertificateResponseDto(
35+
certificate_url=certificate_url,
36+
email=certificate.participant_email or "",
37+
product_id=certificate.product_id,
38+
success=certificate.success or False
39+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from pydantic import BaseModel, Field
2+
import uuid
3+
4+
class DownloadCertificateRequestDto(BaseModel):
5+
id: uuid.UUID = Field(..., description="UUID do certificado")
6+
7+
class DownloadCertificateResponseDto(BaseModel):
8+
certificate_url: str
9+
email: str
10+
product_id: int
11+
success: bool

src/application/dto/fetch_certificate_dto.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class FetchCertificateResponseDto(BaseModel):
2727
participant_email: Optional[str] = None
2828
participant_document: Optional[str] = None
2929
certificate_url: Optional[str] = None
30+
certificate_key: Optional[str] = None
3031
created_at: Optional[str] = None
3132
updated_at: Optional[str] = None
3233
email: Optional[str] = None

src/application/strategy/fetch_certificate_strategy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def execute(self, request: FetchCertificateRequestDto) -> List[FetchCertificateR
5555
participant_email=certificate.participant_email,
5656
participant_document=certificate.participant_cpf,
5757
certificate_url=certificate.certificate_url,
58+
certificate_key=certificate.certificate_key,
5859
created_at=certificate.generated_date,
5960
updated_at=certificate.generated_date, # Assumindo que não temos updated_at separado
6061
email=certificate.participant_email,

src/domain/repository/base_repository.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from abc import ABC, abstractmethod
2-
from typing import List, Optional, TypeVar, Generic
2+
from typing import List, Optional, TypeVar, Generic, Union
33
from pydantic import BaseModel
4+
import uuid
45

56
# Tipo genérico para as entidades
67
T = TypeVar('T', bound=BaseModel)
@@ -21,6 +22,11 @@ def get_by_id(self, entity_id: str) -> Optional[T]:
2122
"""Busca uma entidade pelo ID"""
2223
pass
2324

25+
@abstractmethod
26+
def find_by_id(self, entity_id: Union[str, uuid.UUID]) -> Optional[T]:
27+
"""Busca uma entidade pelo UUID"""
28+
pass
29+
2430
@abstractmethod
2531
def get_all(self) -> List[T]:
2632
"""Busca todas as entidades"""

src/domain/repository/certificate_repository.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from abc import abstractmethod
2-
from typing import List, Optional
2+
from typing import List, Optional, Union
33
from src.domain.entity.certificate import Certificate
44
from src.domain.repository.base_repository import BaseRepository
5+
import uuid
56

67
class CertificateRepository(BaseRepository[Certificate]):
78
"""

src/infrastructure/aws/boto_aws.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
class ServiceNameAWS(Enum):
66
SQS = 'sqs'
77
DYNAMODB = 'dynamodb'
8+
S3 = 's3'
89

910
def get_instance_aws(service_name: ServiceNameAWS):
1011
return client(
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import logging
2+
from src.infrastructure.aws.boto_aws import get_instance_aws, ServiceNameAWS
3+
from src.infrastructure.config.config import config
4+
5+
logger = logging.getLogger(__name__)
6+
logger.setLevel(logging.INFO)
7+
8+
class FileManager:
9+
"""
10+
Gerencia operações de arquivos no S3.
11+
"""
12+
13+
def __init__(self):
14+
self.aws = get_instance_aws(ServiceNameAWS.S3)
15+
self.bucket_name = config.S3_BUCKET_NAME
16+
17+
def get_url(self, key: str, expires_in: int = 604800) -> str:
18+
"""
19+
Gera uma URL pré-assinada para acessar (GET) um objeto no S3.
20+
Configura response-content-disposition=inline para visualização direta no navegador.
21+
22+
Args:
23+
key: Chave do objeto no S3
24+
expires_in: Tempo de expiração em segundos (padrão: 7 dias)
25+
26+
Returns:
27+
URL pré-assinada para acesso ao objeto
28+
29+
Raises:
30+
Exception: Se houver erro ao gerar a URL
31+
"""
32+
try:
33+
logger.info(f"Getting URL for key {key} with expiration {expires_in} seconds")
34+
35+
# Gera URL pré-assinada para GET com response-content-disposition=inline
36+
presigned_url = self.aws.generate_presigned_url(
37+
'get_object',
38+
Params={
39+
'Bucket': self.bucket_name,
40+
'Key': key,
41+
'ResponseContentDisposition': 'inline'
42+
},
43+
ExpiresIn=expires_in
44+
)
45+
46+
logger.info(f"Successfully retrieved URL for key {key}")
47+
return presigned_url
48+
49+
except Exception as e:
50+
logger.error(f"Error generating presigned URL for key {key}: {e}")
51+
return None
52+
53+
def get_certificate_download_url(self, certificate_key: str) -> str:
54+
"""
55+
Gera URL pré-assinada específica para download de certificados.
56+
URL válida por 30 minutos.
57+
58+
Args:
59+
certificate_key: Chave do certificado no S3
60+
61+
Returns:
62+
URL pré-assinada para download do certificado (válida por 30 minutos)
63+
"""
64+
return self.get_url(certificate_key, expires_in=1800) # 30 minutos em segundos
65+

src/infrastructure/config/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
class Config(BaseSettings):
99
REGION: str
1010
BUILDER_QUEUE_URL: str
11+
S3_BUCKET_NAME: str
1112
ENVIRONMENT: str = Field(default="dev")
1213
PROJECT_NAME: str = Field(default="certified-builder-api-py")
1314
URL_SERVICE_TECH: str

src/infrastructure/container/dependency_container.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
from typing import Dict, Any
77
from src.infrastructure.aws.dynamodb_service import DynamoDBService
88
from src.infrastructure.aws.sqs_service import SQSService
9+
from src.infrastructure.aws.file_manager import FileManager
910
from src.infrastructure.repository.certificate_repository_impl import CertificateRepositoryImpl
1011
from src.infrastructure.repository.participant_repository_impl import ParticipantRepositoryImpl
1112
from src.infrastructure.repository.product_repository_impl import ProductRepositoryImpl
1213
from src.infrastructure.repository.order_repository_impl import OrderRepositoryImpl
13-
from src.application.fetch_order_tech_floripa import FetchOrderTechFloripa
14+
1415

1516
class DependencyContainer:
1617
"""
@@ -37,6 +38,7 @@ def _register_services(self):
3738
Define como cada dependência deve ser criada.
3839
"""
3940
# Registra serviços de infraestrutura
41+
self._services['file_manager'] = self._create_file_manager
4042
self._services['dynamodb_service'] = self._create_dynamodb_service
4143
self._services['sqs_service'] = self._create_sqs_service
4244
# Registra repositórios
@@ -49,6 +51,7 @@ def _register_services(self):
4951
self._services['create_certificate'] = self._create_create_certificate
5052
self._services['fetch_certificate'] = self._create_fetch_certificate
5153
self._services['fetch_order_tech_floripa'] = self._create_fetch_order_tech_floripa
54+
self._services['download_certificate'] = self._create_download_certificate
5255

5356
def get(self, service_name: str) -> Any:
5457
"""
@@ -83,6 +86,10 @@ def reset(self):
8386
"""
8487
self._singletons.clear()
8588

89+
def _create_file_manager(self) -> FileManager:
90+
"""Cria uma instância do FileManager."""
91+
return FileManager()
92+
8693
def _create_sqs_service(self) -> SQSService:
8794
"""Cria uma instância do SQSService."""
8895
return SQSService()
@@ -133,7 +140,11 @@ def _create_fetch_certificate(self):
133140
from src.application.fetch_certificate import FetchCertificate
134141
return FetchCertificate()
135142

136-
143+
def _create_download_certificate(self):
144+
"""Cria uma instância do DownloadCertificate."""
145+
from src.application.download_certificate import DownloadCertificate
146+
return DownloadCertificate()
147+
137148
def _create_fetch_order_tech_floripa(self):
138149
"""
139150
Cria uma instância do FetchOrderTechFloripa.

0 commit comments

Comments
 (0)