💻 Exemplos: Capítulo 08: Modelagem Rica de Domínio: Enumerações (Enum), Composição e Dataclasses (slots=True)
Especialização em Sistemas com Python 3.12+ • Capítulo 08
🎯 Demonstração Prática: Gestão de Contratos B2B e Assinaturas com Dataclasses e StrEnum
O exemplo abaixo demonstra a modelagem rica de domínio sob os princípios de DDD tático em Python 3.12+:
- Objetos de Valor imutáveis (
Value Objects) protegidos por@dataclass(frozen=True, slots=True). - Uso de
enum.StrEnumpara estados semânticos serializáveis diretamente. - Entidade de Contrato com composição de múltiplos objetos e validações de invariantes em
__post_init__. - Métodos de transição de estado que preservam as regras de negócio de faturamento recorrente.
"""
demonstracao_dominio_e_dataclasses.py
Modelagem de Domínio Rica (DDD), Value Objects, StrEnum e Dataclasses com Slots.
Compatível com Python 3.12+.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_EVEN
from enum import StrEnum, auto
import re
import uuid
from typing import Final
# ==============================================================================
# 1. ENUMERAÇÕES SEMÂNTICAS (STRENUM PYTHON 3.11+)
# ==============================================================================
class CicloFaturamento(StrEnum):
MENSAL = auto()
TRIMESTRAL = auto()
ANUAL = auto()
class StatusContrato(StrEnum):
RASCUNHO = auto()
EM_ANALISE_RISCO = auto()
VIGENTE = auto()
SUSPENSO_INADIMPLENCIA = auto()
ENCERRADO = auto()
# ==============================================================================
# 2. VALUE OBJECTS (IMUTÁVEIS E AUTOCONTIDOS)
# ==============================================================================
@dataclass(frozen=True, slots=True)
class CNPJ:
valor: str
def __post_init__(self) -> None:
apenas_digitos = re.sub(r"\D", "", self.valor)
if len(apenas_digitos) != 14:
raise ValueError(f"CNPJ inválido: '{self.valor}'. Deve conter exatamente 14 dígitos.")
# Congelamento normalizado
object.__setattr__(self, "valor", apenas_digitos)
def formatado(self) -> str:
d = self.valor
return f"{d[:2]}.{d[2:5]}.{d[5:8]}/{d[8:12]}-{d[12:]}"
@dataclass(frozen=True, slots=True)
class EnderecoComercial:
logradouro: str
numero: str
bairro: str
cidade: str
uf: str
cep: str
# ==============================================================================
# 3. ENTIDADES DE DOMÍNIO E AGREGADOS
# ==============================================================================
@dataclass(slots=True)
class ClienteCorporativo:
id_cliente: uuid.UUID
razao_social: str
cnpj: CNPJ
endereco: EnderecoComercial
email_faturamento: str
@dataclass(slots=True)
class ContratoAssinatura:
id_contrato: str
cliente: ClienteCorporativo
valor_plano: Decimal
ciclo: CicloFaturamento
status: StatusContrato = StatusContrato.RASCUNHO
criado_em: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
itens_inclusos: list[str] = field(default_factory=list)
def __post_init__(self) -> None:
"""Validação de invariantes na instanciação da entidade."""
if self.valor_plano <= Decimal("0.00"):
raise ValueError(f"Valor do contrato deve ser positivo: {self.valor_plano}")
if not self.id_contrato.startswith("CT-"):
raise ValueError(f"Identificador de contrato malformado: {self.id_contrato}")
def submeter_para_analise(self) -> None:
if self.status != StatusContrato.RASCUNHO:
raise RuntimeError(f"Transição inválida: Não pode submeter a partir do status [{self.status.value}]")
self.status = StatusContrato.EM_ANALISE_RISCO
def aprovar_e_ativar(self) -> None:
if self.status != StatusContrato.EM_ANALISE_RISCO:
raise RuntimeError(f"Apenas contratos em análise podem ser ativados diretamente.")
self.status = StatusContrato.VIGENTE
def aplicar_reajuste_anual(self, percentual_indice: Decimal) -> Decimal:
"""Aplica índice inflacionário anual sobre a mensalidade do plano."""
if self.status != StatusContrato.VIGENTE:
raise RuntimeError("Apenas contratos vigentes admitem reajuste anual de índice.")
fator_reajuste = Decimal("1.00") + percentual_indice
self.valor_plano = (self.valor_plano * fator_reajuste).quantize(Decimal("0.01"), ROUND_HALF_EVEN)
return self.valor_plano
# ==============================================================================
# 4. EXECUÇÃO DEMONSTRATIVA
# ==============================================================================
if __name__ == "__main__":
print("=" * 80)
print(" GESTÃO DE CONTRATOS SAAS COM DATACLASSES E VALUE OBJECTS")
print("=" * 80)
# Criação dos Value Objects
cnpj_cliente = CNPJ("45.123.789/0001-92")
endereco_sede = EnderecoComercial(
logradouro="Avenida Brigadeiro Faria Lima",
numero="3477",
bairro="Itaim Bibi",
cidade="São Paulo",
uf="SP",
cep="04538-133"
)
# Criação da Entidade Cliente
cliente_tech = ClienteCorporativo(
id_cliente=uuid.uuid4(),
razao_social="OmniCloud Infraestrutura e Inteligência Ltda",
cnpj=cnpj_cliente,
endereco=endereco_sede,
email_faturamento="billing@omnicloud.com.br"
)
# Criação do Contrato Agregado
contrato = ContratoAssinatura(
id_contrato="CT-2026-9042",
cliente=cliente_tech,
valor_plano=Decimal("12500.00"),
ciclo=CicloFaturamento.ANUAL,
itens_inclusos=["Cluster Kubernetes Dedicado", "Suporte 24/7 Nível 3", "Backup Geodistribuído"]
)
print(f"Contrato ID : {contrato.id_contrato}")
print(f"Cliente Titular : {contrato.cliente.razao_social}")
print(f"CNPJ Formatado : {contrato.cliente.cnpj.formatado()}")
print(f"Sede Operacional : {contrato.cliente.endereco.cidade}/{contrato.cliente.endereco.uf}")
print(f"Status Inicial : [{contrato.status.value}]")
print(f"Valor Contratado : R$ {contrato.valor_plano:>10.2f} ({contrato.ciclo.value.upper()})")
print("-" * 80)
print(">> Avançando ciclo de vida contratual...")
contrato.submeter_para_analise()
print(f"Status Atualizado : [{contrato.status.value}]")
contrato.aprovar_e_ativar()
print(f"Status Atualizado : [{contrato.status.value}] (Contrato em Plena Vigência)")
print("-" * 80)
print(">> Aplicando reajuste IPCA de 4.85% após 12 meses de vigência...")
novo_valor = contrato.aplicar_reajuste_anual(Decimal("0.0485"))
print(f"Novo Valor Mensal : R$ {novo_valor:>10.2f} (Atualizado com precisão contábil)")
print("=" * 80)