from pathlib import Path
import math
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_LEFT
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.platypus import (
BaseDocTemplate, Frame, PageBreak, PageTemplate, Paragraph, Spacer,
Table, TableStyle, KeepTogether, Flowable,
)
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / "output/pdf/dtf-plano-producao-e-roadmap.pdf"
FONT = "/usr/share/fonts/TTF/DejaVuSans.ttf"
FONT_BOLD = "/usr/share/fonts/TTF/DejaVuSans-Bold.ttf"
pdfmetrics.registerFont(TTFont("DejaVu", FONT))
pdfmetrics.registerFont(TTFont("DejaVu-Bold", FONT_BOLD))
NAVY = colors.HexColor("#071426")
BLUE = colors.HexColor("#1463D8")
SKY = colors.HexColor("#EAF2FF")
ORANGE = colors.HexColor("#FF8A1F")
GREEN = colors.HexColor("#16825B")
RED = colors.HexColor("#B83737")
INK = colors.HexColor("#162235")
MUTED = colors.HexColor("#607085")
LINE = colors.HexColor("#D9E1EA")
PALE = colors.HexColor("#F6F8FB")
WHITE = colors.white
class Rule(Flowable):
def __init__(self, color=ORANGE, width=4.0, length=54):
Flowable.__init__(self)
self.color, self.thickness, self.length = color, width, length
self.width, self.height = length, width
def draw(self):
self.canv.setStrokeColor(self.color)
self.canv.setLineWidth(self.thickness)
self.canv.line(0, self.thickness / 2, self.length, self.thickness / 2)
class ArchitectureDiagram(Flowable):
"""Small three-zone diagram drawn in vector primitives."""
def __init__(self):
Flowable.__init__(self)
self.width, self.height = 17.0 * cm, 7.2 * cm
def box(self, x, y, w, h, title, sub, fill, accent):
c = self.canv
c.setFillColor(fill)
c.setStrokeColor(accent)
c.setLineWidth(1)
c.roundRect(x, y, w, h, 7, fill=1, stroke=1)
c.setFillColor(INK)
c.setFont("DejaVu-Bold", 8.3)
c.drawString(x + 8, y + h - 16, title)
c.setFillColor(MUTED)
c.setFont("DejaVu", 6.6)
for i, line in enumerate(sub.split("\n")):
c.drawString(x + 8, y + h - 29 - 9 * i, line)
def _arrowhead(self, tip_x, tip_y, from_x, from_y):
c = self.canv
dx, dy = from_x - tip_x, from_y - tip_y
distance = math.hypot(dx, dy)
if not distance:
return
ux, uy = dx / distance, dy / distance
angle = math.radians(28)
arm = 5
for sign in (-1, 1):
vx = ux * math.cos(angle) - sign * uy * math.sin(angle)
vy = uy * math.cos(angle) + sign * ux * math.sin(angle)
c.line(tip_x, tip_y, tip_x + arm * vx, tip_y + arm * vy)
def arrow(self, x1, y1, x2, y2, label=None):
c = self.canv
c.setStrokeColor(colors.HexColor("#8BA0B8"))
c.setFillColor(colors.HexColor("#8BA0B8"))
c.setLineWidth(1.1)
c.line(x1, y1, x2, y2)
self._arrowhead(x2, y2, x1, y1)
if label:
c.setFillColor(MUTED)
c.setFont("DejaVu", 5.9)
c.drawCentredString((x1 + x2) / 2, (y1 + y2) / 2 + 4, label)
def double_arrow(self, x1, y1, x2, y2, label=None):
c = self.canv
c.setStrokeColor(colors.HexColor("#8BA0B8"))
c.setFillColor(colors.HexColor("#8BA0B8"))
c.setLineWidth(1.1)
c.line(x1, y1, x2, y2)
self._arrowhead(x1, y1, x2, y2)
self._arrowhead(x2, y2, x1, y1)
if label:
c.setFillColor(MUTED)
c.setFont("DejaVu", 5.9)
c.drawCentredString((x1 + x2) / 2, (y1 + y2) / 2 + 4, label)
def draw(self):
c = self.canv
zones = [(0, 0, 142, "CLIENTE E INTEGRAÇÕES"), (152, 0, 214, "NUVEM / VPS"),
(376, 0, 106, "EQUIPE DA FÁBRICA")]
for x, y, w, name in zones:
c.setFillColor(PALE)
c.setStrokeColor(LINE)
c.roundRect(x, 0, w, self.height, 8, fill=1, stroke=1)
c.setFillColor(MUTED)
c.setFont("DejaVu-Bold", 6.4)
c.drawString(x + 9, self.height - 13, name)
self.box(12, 117, 120, 40, "Site DTF", "arte, preço e checkout\nacompanhamento do pedido", WHITE, ORANGE)
self.box(12, 48, 120, 40, "Integrações externas", "Mercado Pago · Tiny/Olist\nWhatsApp Business", WHITE, BLUE)
self.box(162, 117, 88, 40, "API FastAPI", "pedido, upload\ne integrações", SKY, BLUE)
self.box(258, 117, 92, 40, "R2", "originais e\narte final privada", SKY, BLUE)
self.box(162, 48, 88, 40, "Worker", "antivírus e\npré-flight", SKY, GREEN)
self.box(258, 48, 92, 40, "PostgreSQL", "pedidos, jobs\ne histórico", SKY, BLUE)
self.box(388, 117, 82, 40, "Kanban online", "fila, status e\ndownload seguro", WHITE, colors.HexColor("#7650B8"))
self.box(388, 48, 82, 40, "Operador", "baixa e importa\nno FlexiPRINT", WHITE, colors.HexColor("#7650B8"))
self.arrow(132, 137, 162, 137)
self.double_arrow(132, 88, 162, 117)
self.arrow(250, 137, 258, 137)
self.arrow(206, 117, 206, 88)
self.double_arrow(250, 117, 258, 88)
self.double_arrow(250, 68, 258, 68)
self.arrow(350, 137, 388, 137)
self.arrow(429, 117, 429, 88)
def paragraph(text, style):
return Paragraph(text, style)
def bullet(text, styles):
return Paragraph(f"• {text}", styles["bullet"])
def section_title(text, styles, kicker=None):
blocks = []
if kicker:
blocks.append(Paragraph(kicker.upper(), styles["kicker"]))
blocks.append(Spacer(1, 3))
blocks += [Paragraph(text, styles["dtf_h1"]), Spacer(1, 7), Rule(), Spacer(1, 12)]
return blocks
def on_page(canvas, doc):
if doc.page == 1:
return
canvas.saveState()
canvas.setStrokeColor(LINE)
canvas.line(doc.leftMargin, A4[1] - 1.25 * cm, A4[0] - doc.rightMargin, A4[1] - 1.25 * cm)
canvas.setFont("DejaVu", 7.4)
canvas.setFillColor(MUTED)
canvas.drawString(doc.leftMargin, A4[1] - 1.05 * cm, "DTF Dropstar - plano de entrega")
canvas.drawRightString(A4[0] - doc.rightMargin, 0.82 * cm, f"Página {doc.page}")
canvas.restoreState()
def build_pdf():
styles = getSampleStyleSheet()
styles.add(ParagraphStyle(name="cover_kicker", fontName="DejaVu-Bold", fontSize=10,
leading=13, textColor=ORANGE, spaceAfter=10, letterSpacing=0.6))
styles.add(ParagraphStyle(name="cover_title", fontName="DejaVu-Bold", fontSize=30,
leading=35, textColor=WHITE, spaceAfter=11))
styles.add(ParagraphStyle(name="cover_text", fontName="DejaVu", fontSize=12,
leading=17, textColor=colors.HexColor("#D6E0EF")))
styles.add(ParagraphStyle(name="kicker", fontName="DejaVu-Bold", fontSize=8,
leading=10, textColor=ORANGE, letterSpacing=0.7))
styles.add(ParagraphStyle(name="dtf_h1", fontName="DejaVu-Bold", fontSize=20,
leading=24, textColor=NAVY, spaceAfter=0))
styles.add(ParagraphStyle(name="dtf_h2", fontName="DejaVu-Bold", fontSize=12,
leading=15, textColor=INK, spaceBefore=6, spaceAfter=5))
styles.add(ParagraphStyle(name="body", fontName="DejaVu", fontSize=9.3,
leading=14, textColor=INK, spaceAfter=8))
styles.add(ParagraphStyle(name="bullet", fontName="DejaVu", fontSize=9.1,
leading=13, textColor=INK, leftIndent=2, spaceAfter=4))
styles.add(ParagraphStyle(name="small", fontName="DejaVu", fontSize=7.6,
leading=10.5, textColor=MUTED))
styles.add(ParagraphStyle(name="cell", fontName="DejaVu", fontSize=7.7,
leading=10, textColor=INK))
styles.add(ParagraphStyle(name="cell_bold", fontName="DejaVu-Bold", fontSize=7.7,
leading=10, textColor=INK))
styles.add(ParagraphStyle(name="cell_white", fontName="DejaVu-Bold", fontSize=7.7,
leading=10, textColor=WHITE))
styles.add(ParagraphStyle(name="note", fontName="DejaVu", fontSize=8.2,
leading=12, textColor=INK, backColor=colors.HexColor("#FFF4E7"),
borderColor=colors.HexColor("#F7D2A5"), borderWidth=0.5,
borderPadding=8, borderRadius=4))
doc = BaseDocTemplate(str(OUT), pagesize=A4, leftMargin=1.65 * cm, rightMargin=1.65 * cm,
topMargin=1.7 * cm, bottomMargin=1.45 * cm)
frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="main")
doc.addPageTemplates([PageTemplate(id="main", frames=[frame], onPage=on_page)])
story = []
# Cover
story.append(Spacer(1, 2.4 * cm))
cover = [
Paragraph("PLANO DE ENTREGA", styles["cover_kicker"]),
Paragraph("Sistema DTF
Plano de entrega", styles["cover_title"]),
Paragraph("Uma operação em que o cliente envia a arte, recebe análise, paga e acompanha a produção sem depender de WhatsApp, pastas ou repasse manual.", styles["cover_text"]),
Spacer(1, 28),
Table([[paragraph("OBJETIVO", styles["cover_kicker"]), paragraph("DECISÃO DE ARQUITETURA", styles["cover_kicker"])],
[paragraph("Transformar capacidade ociosa em atendimento e produção 24 horas.", styles["cover_text"]),
paragraph("Portal e Kanban online + Cloudflare R2 + VPS existente. A impressão continua manual na fábrica.", styles["cover_text"])]],
colWidths=[8.1 * cm, 8.1 * cm], style=TableStyle([
("BACKGROUND", (0, 0), (-1, -1), NAVY),
("BOX", (0, 0), (-1, -1), 0.6, colors.HexColor("#40516B")),
("INNERGRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#40516B")),
("LEFTPADDING", (0, 0), (-1, -1), 13), ("RIGHTPADDING", (0, 0), (-1, -1), 13),
("TOPPADDING", (0, 0), (-1, -1), 11), ("BOTTOMPADDING", (0, 0), (-1, -1), 12),
])),
Spacer(1, 0.7 * cm),
Paragraph("Consolidação das discussões técnicas e comerciais - setembro de 2026", styles["cover_text"]),
]
cover_table = Table([[cover]], colWidths=[17.7 * cm], rowHeights=[23.6 * cm], style=TableStyle([
("BACKGROUND", (0, 0), (-1, -1), NAVY),
("LEFTPADDING", (0, 0), (-1, -1), 1.15 * cm),
("RIGHTPADDING", (0, 0), (-1, -1), 1.15 * cm),
("TOPPADDING", (0, 0), (-1, -1), 1.05 * cm),
("BOTTOMPADDING", (0, 0), (-1, -1), 1.05 * cm),
]))
story.append(cover_table)
story.append(PageBreak())
# Context
story += section_title("O problema que vamos resolver", styles, "Contexto")
story.append(paragraph(
"A sala de DTF tem capacidade instalada muito maior que a produção atual. O gargalo não é a máquina: é o fluxo entre o pedido, a arte, o designer e a sala. Hoje o cliente envia arquivo no WhatsApp, o atendimento repassa, o designer descobre problemas tarde e o status fica escondido em pastas de rede.",
styles["body"]))
story.append(paragraph("O projeto resolve quatro falhas do processo:", styles["dtf_h2"]))
for item in [
"Recebimento limitado pelo horário: um cliente que chega no fim do dia espera até o turno seguinte.",
"Arquivo sem rastreabilidade: WhatsApp e nomes digitados manualmente facilitam perda e erro.",
"Arquivo ruim chega tarde ao designer: qualidade, dimensão e transparência são verificadas depois de consumir tempo humano.",
"Produção sem números: não há registro confiável de espera, velocidade real, fila ou retrabalho.",
]:
story.append(bullet(item, styles))
story.append(Spacer(1, 7))
decision = Table([
[paragraph("ANTES", styles["cell_bold"]), paragraph("DEPOIS", styles["cell_bold"])],
[paragraph("Pedido e arte circulam por WhatsApp e pastas.", styles["cell"]), paragraph("Site DTF recebe a arte, analisa, precifica e registra o fluxo.", styles["cell"])],
[paragraph("Designer corrige indiscriminadamente.", styles["cell"]), paragraph("Arte pronta segue rápido; exceções vão para atendimento humano.", styles["cell"])],
[paragraph("Sala não enxerga fila real nem tempos.", styles["cell"]), paragraph("Kanban online organiza a fila e registra cada movimento manualmente.", styles["cell"])],
], colWidths=[8.6 * cm, 8.6 * cm], style=TableStyle([
("BACKGROUND", (0, 0), (-1, 0), SKY), ("GRID", (0, 0), (-1, -1), 0.45, LINE),
("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (-1, -1), 9),
("RIGHTPADDING", (0, 0), (-1, -1), 9), ("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
]))
story.append(decision)
story.append(PageBreak())
# Flow
story += section_title("Como será a jornada do pedido", styles, "Modelo operacional")
steps = [
("1", "Cliente entra no site DTF", "Escolhe o produto, envia a arte e vê o valor conforme as regras comerciais já configuradas."),
("2", "Sistema registra a arte", "Calcula a metragem e prepara o pedido. O pré-flight automático será validado depois da entrega, com arquivos reais."),
("3", "Cliente paga", "O checkout é do Mercado Pago; os dados do cartão não passam pelo nosso servidor."),
("4", "Pedido entra no sistema", "Depois da confirmação do pagamento, o pedido é criado no Tiny e aparece no Kanban."),
("5", "Equipe baixa e imprime", "O operador baixa o arquivo final no Kanban, importa no FlexiPRINT e atualiza o card."),
]
rows = []
for n, title, text in steps:
rows.append([
paragraph(n, ParagraphStyle("num" + n, parent=styles["cell_bold"], fontSize=13, textColor=ORANGE, alignment=TA_CENTER)),
paragraph(title, styles["cell_bold"]),
paragraph(text, styles["cell"]),
])
story.append(Table(rows, colWidths=[1.0 * cm, 5.3 * cm, 10.9 * cm], style=TableStyle([
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
("BACKGROUND", (0, 0), (0, -1), PALE), ("LEFTPADDING", (0, 0), (-1, -1), 9),
("RIGHTPADDING", (0, 0), (-1, -1), 9), ("TOPPADDING", (0, 0), (-1, -1), 10),
("BOTTOMPADDING", (0, 0), (-1, -1), 10),
])))
story.append(Spacer(1, 15))
story.append(paragraph("Onde o WhatsApp entra", styles["dtf_h2"]))
story.append(paragraph(
"O WhatsApp deixa de ser o lugar onde a arte é enviada. Ele serve para avisar o cliente: pagamento aprovado, pedido em produção, pedido pronto ou necessidade de correção. Quando houver problema, a mensagem leva o cliente de volta para o site.", styles["body"]))
story.append(paragraph(
"Cuidados na automação: se Mercado Pago, Tiny ou WhatsApp reenviar um aviso por falha, o sistema confere antes de agir. Assim, não cria dois pedidos nem manda a mesma mensagem duas vezes.", styles["note"]))
story.append(PageBreak())
# Architecture
story += section_title("Arquitetura de software e infraestrutura", styles, "Produção")
story.append(paragraph(
"Todo o sistema roda na nuvem: recebe, processa, integra e disponibiliza o arquivo final. A fábrica acessa o Kanban pelo navegador e baixa o arquivo quando for produzir; não há componente instalado na rede interna.", styles["body"]))
story.append(ArchitectureDiagram())
story.append(Spacer(1, 12))
columns = [
[paragraph("NUVEM", styles["cell_bold"]), paragraph("Site DTF, Kanban online, API FastAPI, worker, ClamAV, PostgreSQL e Cloudflare R2.", styles["cell"])],
[paragraph("FÁBRICA", styles["cell_bold"]), paragraph("Navegador, operador e FlexiPRINT atual. O arquivo é baixado e importado manualmente.", styles["cell"])],
[paragraph("TERCEIROS", styles["cell_bold"]), paragraph("Mercado Pago, Tiny/Olist e WhatsApp Business API.", styles["cell"])],
]
story.append(Table(columns, colWidths=[3.0 * cm, 14.2 * cm], style=TableStyle([
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"),
("BACKGROUND", (0, 0), (0, -1), SKY), ("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8), ("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
])))
story.append(Spacer(1, 10))
for item in [
"Upload grande: o navegador manda o arquivo direto para o R2, em partes. O VPS não precisa receber 5 GB de uma vez.",
"Fila: o banco guarda o que precisa ser analisado, enviado ao Tiny, avisado no WhatsApp ou apagado no prazo certo.",
"Segurança: o arquivo entra em quarentena, passa pelo antivírus e só depois é analisado e liberado no Kanban.",
"Se a fábrica ficar sem internet: o site continua no ar. A equipe apenas não consegue baixar arquivo novo ou atualizar o Kanban até a conexão voltar.",
]:
story.append(bullet(item, styles))
story.append(PageBreak())
# Storage/costs
story += section_title("R2: custos e retenção", styles, "Cloudflare")
story.append(paragraph(
"O R2 é onde os arquivos ficam guardados. Ele é privado: o operador só baixa pelo Kanban, com link temporário. O custo vem principalmente da média de GB guardados no mês; leitura e gravação costumam pesar bem menos. O download não tem cobrança de saída de dados.", styles["body"]))
cost = [
[paragraph("MÉDIA ARMAZENADA", styles["cell_white"]), paragraph("R2 / MÊS", styles["cell_white"]), paragraph("APROX. EM R$*", styles["cell_white"])],
[paragraph("100 GB", styles["cell"]), paragraph("US$ 1,35", styles["cell"]), paragraph("R$ 7", styles["cell"])],
[paragraph("500 GB", styles["cell"]), paragraph("US$ 7,35", styles["cell"]), paragraph("R$ 38", styles["cell"])],
[paragraph("1 TB", styles["cell"]), paragraph("US$ 14,85", styles["cell"]), paragraph("R$ 76", styles["cell"])],
[paragraph("3 TB", styles["cell"]), paragraph("US$ 44,85", styles["cell"]), paragraph("R$ 230", styles["cell"])],
]
story.append(Table(cost, colWidths=[6.0 * cm, 5.2 * cm, 6.0 * cm], style=TableStyle([
("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("LEFTPADDING", (0, 0), (-1, -1), 9),
("RIGHTPADDING", (0, 0), (-1, -1), 9), ("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
])))
story.append(Spacer(1, 5))
story.append(paragraph("* Referência de US$ 1 = R$ 5,13; câmbio, impostos e tarifas do meio de pagamento podem variar.", styles["small"]))
story.append(Spacer(1, 10))
retention = [
[paragraph("ARTEFATO", styles["cell_bold"]), paragraph("PRAZO", styles["cell_bold"]), paragraph("REGRA", styles["cell_bold"])],
[paragraph("Upload multipart incompleto", styles["cell"]), paragraph("1 dia", styles["cell"]), paragraph("Abortado automaticamente.", styles["cell"])],
[paragraph("Recusado ou infectado", styles["cell"]), paragraph("3 dias", styles["cell"]), paragraph("Suporte pode conferir; depois apaga.", styles["cell"])],
[paragraph("Original do cliente", styles["cell"]), paragraph("até 7 dias", styles["cell"]), paragraph("Após aprovação, não fica guardado sem necessidade.", styles["cell"])],
[paragraph("Arte pronta para impressão", styles["cell"]), paragraph("máx. 30 dias", styles["cell"]), paragraph("Prazo conta do primeiro upload; permite correção ou reimpressão recente.", styles["cell"])],
[paragraph("Histórico e métricas", styles["cell"]), paragraph("banco", styles["cell"]), paragraph("Sem manter a imagem.", styles["cell"])],
]
story.append(Table(retention, colWidths=[5.2 * cm, 2.8 * cm, 9.2 * cm], style=TableStyle([
("BACKGROUND", (0, 0), (-1, 0), SKY), ("GRID", (0, 0), (-1, -1), 0.45, LINE),
("VALIGN", (0, 0), (-1, -1), "TOP"), ("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8), ("TOPPADDING", (0, 0), (-1, -1), 7),
("BOTTOMPADDING", (0, 0), (-1, -1), 7),
])))
story.append(Spacer(1, 9))
story.append(paragraph("Decisão: não haverá biblioteca infinita de artes. Após o prazo, o cliente reenvia o arquivo; o pedido e sua rastreabilidade permanecem registrados.", styles["note"]))
story.append(PageBreak())
# Production / CI CD
story += section_title("Como fica em produção", styles, "Operação, publicação e continuidade")
story.append(paragraph(
"A primeira entrega será operada como uma única aplicação na VPS. Portal, Kanban, processamento e banco seguem o mesmo ciclo de publicação, com acesso da fábrica exclusivamente pelo navegador. Isso reduz pontos de manutenção e mantém a operação independente da rede interna.", styles["body"]))
stacks = [
[paragraph("AMBIENTE", styles["cell_white"]), paragraph("COMPONENTES", styles["cell_white"]), paragraph("OPERAÇÃO", styles["cell_white"])],
[paragraph("Sistema DTF", styles["cell_bold"]), paragraph("site DTF, Kanban online, API, processamento, antivírus, banco de dados e cópia diária de segurança", styles["cell"]), paragraph("VPS com armazenamento persistente; operação e acompanhamento centralizados", styles["cell"])],
]
story.append(Table(stacks, colWidths=[3.7 * cm, 9.1 * cm, 4.4 * cm], style=TableStyle([
("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"),
("LEFTPADDING", (0, 0), (-1, -1), 8), ("RIGHTPADDING", (0, 0), (-1, -1), 8),
("TOPPADDING", (0, 0), (-1, -1), 8), ("BOTTOMPADDING", (0, 0), (-1, -1), 8),
])))
story.append(Spacer(1, 13))
story.append(paragraph("Publicação com controle", styles["dtf_h2"]))
for item in [
"Toda alteração passa por testes automáticos antes de poder chegar ao ambiente de produção.",
"Cada publicação fica identificada pela versão do código, permitindo restaurar rapidamente a versão anterior se houver qualquer problema.",
"As mudanças são conferidas primeiro em ambiente de validação e só então liberadas para a operação.",
"Ao final da publicação, uma verificação confirma que os serviços essenciais voltaram a responder antes de encerrar o processo.",
"Credenciais de storage, pagamentos, integrações e banco permanecem fora do código e são tratadas como dados restritos do ambiente.",
]:
story.append(bullet(item, styles))
story.append(Spacer(1, 9))
story.append(paragraph("Acompanhamento operacional", styles["dtf_h2"]))
story.append(Spacer(1, 5))
story.append(paragraph(
"A rotina de operação acompanha disponibilidade do site e do Kanban, andamento da fila de processamento, falhas de integração e resultado das cópias de segurança. Assim, qualquer desvio é identificado antes de interromper o atendimento ou a produção.", styles["note"]))
story.append(PageBreak())
# Roadmap
story += section_title("Roadmap de implementação", styles, "MVP em até 3 semanas")
story.append(paragraph("O cronograma considera a base de código existente e dedicação integral. A prioridade é concluir o fluxo completo - envio, pagamento, frete, pedido e acompanhamento - antes de qualquer integração com as máquinas. As regras comerciais seguem exatamente o que já está configurado no Site DTF.", styles["body"]))
roadmap = [
[paragraph("QUANDO", styles["cell_white"]), paragraph("ENTREGA", styles["cell_white"]), paragraph("RESULTADO", styles["cell_white"])],
[paragraph("Sem. 1", styles["cell_bold"]), paragraph("Infraestrutura e upload", styles["cell"]), paragraph("VPS, domínio, banco, R2, upload multipart, antivírus e base do serviço.", styles["cell"])],
[paragraph("Sem. 2", styles["cell_bold"]), paragraph("Pagamento, frete e pedido", styles["cell"]), paragraph("Mercado Pago, cotação de frete, criação idempotente no Tiny, geração do arquivo final e estados principais do Kanban.", styles["cell"])],
[paragraph("Sem. 3", styles["cell_bold"]), paragraph("Kanban e entrega", styles["cell"]), paragraph("Kanban online, download seguro, mensagens de status no WhatsApp, retenção de 30 dias e orientação à operação.", styles["cell"])],
]
story.append(Table(roadmap, colWidths=[2.1 * cm, 5.0 * cm, 10.1 * cm], style=TableStyle([
("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("GRID", (0, 0), (-1, -1), 0.45, LINE), ("VALIGN", (0, 0), (-1, -1), "TOP"),
("BACKGROUND", (0, 1), (0, -1), PALE), ("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8), ("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
])))
story.append(Spacer(1, 14))
story.append(paragraph("Fora da primeira entrega", styles["dtf_h2"]))
story.append(Spacer(1, 5))
story.append(paragraph("A primeira entrega termina no download do arquivo final pelo Kanban e na orientação de uso à operação. Integração direta com FlexiPRINT, agente dentro da fábrica, pasta monitorada, painel de sala e relatórios avançados não fazem parte deste prazo. O pré-flight automático será validado depois da entrega, com arquivos e impressão reais. A fábrica mantém esses recursos no ambiente atual; após a entrega, damos suporte e avaliamos mudanças conforme a necessidade.", styles["note"]))
story.append(Spacer(1, 10))
story.append(paragraph("O que fica com a operação", styles["dtf_h2"]))
for item in [
"Acessar o Kanban, baixar o arquivo final e importá-lo manualmente no FlexiPRINT atual.",
"Realizar os testes de impressão e informar qualquer diferença encontrada no arquivo ou no resultado impresso.",
"Manter computadores, FlexiPRINT, perfis de impressão, impressoras e eventuais pastas internas funcionando.",
"A conexão local é necessária apenas para a equipe acessar o Kanban e baixar o arquivo na fábrica. Se ela cair, portal, pagamentos, uploads e fila continuam ativos na nuvem; a produção retoma quando o acesso local voltar.",
"Comunicar mudanças de preços, frete ou regras operacionais que precisem ser refletidas no sistema.",
]:
story.append(bullet(item, styles))
story.append(Spacer(1, 7))
story.append(paragraph("O que precisamos para começar", styles["dtf_h2"]))
for item in [
"Credenciais do Mercado Pago, incluindo a configuração do webhook para confirmação de pagamento.",
"Dados da integração de frete: plataforma(s) já utilizada(s), acesso à API, CEP de origem, serviços oferecidos, regra de cobrança e referência de peso e dimensões por pacote.",
]:
story.append(bullet(item, styles))
story.append(Spacer(1, 7))
story.append(paragraph("Antes de começar", styles["dtf_h2"]))
story.append(Spacer(1, 5))
story.append(paragraph("Com esses acessos e dados liberados, a primeira semana pode começar. A tabela, os descontos e as regras comerciais já existentes no Site DTF serão mantidos. Tiny/Olist e WhatsApp já estão disponíveis; durante o desenvolvimento será definida a ligação técnica dessas integrações com os eventos do pedido. A fábrica valida a impressão depois, baixando o arquivo final pelo Kanban.", styles["note"]))
doc.build(story)
if __name__ == "__main__":
OUT.parent.mkdir(parents=True, exist_ok=True)
build_pdf()
print(OUT)