@danhachuel/prodmaker 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  Todas as versões seguem a etiqueta visual `MAJOR.MINOR.PATCH` com patch de dois dígitos
4
4
  (o registry NPM normaliza `0.1.00` → `0.1.0`).
5
5
 
6
+ ## 0.1.12 — 2026-10-31
7
+
8
+ - **Parte 10 — Polish E2E + OAuth Google 1 clique + radar Supabase + ROAS real.**
9
+ - **OAuth Google (Contas do Google real):** `GET /api/google/oauth/url` monta o consentimento
10
+ (scope adwords, access_type=offline, prompt=consent) e `GET /api/google/callback` troca o
11
+ código e **salva o refresh_token automaticamente** em Chaves de API. Página "Contas do
12
+ Google" com status de conexão, botão conectar/reconectar (popup) e instruções do redirect
13
+ `http://localhost:8188/api/google/callback`. Basta colar Client ID/Secret uma vez.
14
+ - **Radar Supabase (camada pronta, multi-tenant NÃO ativado):** `dbengine.py` abstrai o banco —
15
+ SQLite continua o padrão; definindo `DATABASE_URL` (PostgreSQL/Supabase) o sistema conecta
16
+ via psycopg (dependência declarada, import preguiçoso), com tradução de placeholders `?`→`%s`
17
+ e linhas dict (mesmo acesso por coluna nos serviços). Todas as 12 tabelas passaram a ter
18
+ `user_id` (migração ALTER aplicada automaticamente nos bancos existentes) — base para o
19
+ isolamento multi-tenant via Supabase Auth na próxima etapa.
20
+ - **ROAS real:** tabela `investimentos` (gasto diário em tráfego) + registro/remoção no
21
+ Dashboard; cards **Investimento**, **ROAS** (Receita ÷ Investimento) e **Lucro Líquido**
22
+ (Receita − Investimento) calculados com dados reais — sem investimento registrado, ROAS
23
+ mostra "—" (nunca inventado).
24
+ - **Contas do Google** saiu do placeholder — menu 100% implementado.
25
+
6
26
  ## 0.1.11 — 2026-10-30
7
27
 
8
28
  - **Parte 9 — Pinterest (orgânico), Logs unificado e FAQ/Documentação** — fecha o menu do AdsX.
@@ -0,0 +1,45 @@
1
+ """Contas do Google — OAuth 1 clique (URL de consentimento + callback)."""
2
+ from fastapi import APIRouter, HTTPException, Request
3
+ from fastapi.responses import HTMLResponse, RedirectResponse
4
+
5
+ from ..services import oauth_google
6
+ from ..services.llm import LLMError
7
+
8
+ router = APIRouter(prefix="/api/google", tags=["google-oauth"])
9
+
10
+
11
+ @router.get("/oauth/url")
12
+ def oauth_url(request: Request):
13
+ """Monta a URL de consentimento do Google (abrir em popup/aba)."""
14
+ try:
15
+ return oauth_google.build_auth_url(request)
16
+ except LLMError as exc:
17
+ raise HTTPException(status_code=400, detail=str(exc))
18
+
19
+
20
+ @router.get("/callback")
21
+ def oauth_callback(request: Request, code: str = "", error: str = ""):
22
+ """Callback do OAuth do Google — troca o code e salva o refresh_token."""
23
+ if error:
24
+ return _result_page(False, f"Google retornou erro: {error}")
25
+ if not code:
26
+ return _result_page(False, "Callback sem código de autorização.")
27
+ try:
28
+ oauth_google.exchange_code(code, request)
29
+ except LLMError as exc:
30
+ return _result_page(False, str(exc))
31
+ return _result_page(True, "Conta Google conectada! O Refresh Token foi salvo automaticamente em Chaves de API.")
32
+
33
+
34
+ def _result_page(ok: bool, mensagem: str) -> HTMLResponse:
35
+ cor = "#10b981" if ok else "#ef4444"
36
+ titulo = "Conectado" if ok else "Falha na conexão"
37
+ return HTMLResponse(
38
+ f"""
39
+ <!DOCTYPE html><html lang="pt-BR"><head><meta charset="UTF-8"><title>ProdMaker — Google OAuth</title>
40
+ <style>body{{font-family:-apple-system,'Segoe UI',Roboto,sans-serif;background:#f8fafc;display:flex;min-height:100vh;align-items:center;justify-content:center;margin:0}}
41
+ .box{{background:#fff;border:1px solid #e2e8f0;border-radius:12px;padding:36px;max-width:460px;text-align:center}}
42
+ .ico{{font-size:40px}} h1{{color:{cor};font-size:20px;margin:12px 0}} p{{color:#64748b;font-size:14px}}
43
+ </style></head><body><div class="box"><div class="ico">{'✅' if ok else '❌'}</div><h1>{titulo}</h1><p>{mensagem}</p>
44
+ <p style="font-size:12px;margin-top:16px">Pode fechar esta janela e voltar ao ProdMaker.</p></div></body></html>"""
45
+ )
@@ -0,0 +1,37 @@
1
+ """Investimento em tráfego — endpoints (registro diário para ROAS real)."""
2
+ from fastapi import APIRouter
3
+ from pydantic import BaseModel
4
+
5
+ from ..services import invest
6
+ from ..services.llm import LLMError
7
+ from fastapi import HTTPException
8
+
9
+ router = APIRouter(prefix="/api/investimentos", tags=["investimentos"])
10
+
11
+
12
+ class InvestBody(BaseModel):
13
+ valor: float
14
+ data: str = ""
15
+ observacao: str = ""
16
+
17
+
18
+ @router.get("")
19
+ def listar():
20
+ return {"registros": invest.listar(), "resumo": invest.resumo_financeiro()}
21
+
22
+
23
+ @router.post("")
24
+ def registrar(body: InvestBody):
25
+ try:
26
+ return invest.registrar(body.valor, body.data, body.observacao)
27
+ except LLMError as exc:
28
+ raise HTTPException(status_code=400, detail=str(exc))
29
+
30
+
31
+ @router.delete("/{investimento_id}")
32
+ def remover(investimento_id: int):
33
+ try:
34
+ invest.remover(investimento_id)
35
+ except LLMError as exc:
36
+ raise HTTPException(status_code=404, detail=str(exc))
37
+ return {"ok": True}
package/app/main.py CHANGED
@@ -15,7 +15,7 @@ from .services.database import ensure_db, get_db, now_str, row_to_dict
15
15
  from .services.llm import LLMError, analyze_product, list_providers
16
16
  from .services.store import PRODMAKER_HOME, STORAGE, STATE, ensure_state_dir, get_master_code, get_settings, save_settings
17
17
 
18
- APP_VERSION = "0.1.11"
18
+ APP_VERSION = "0.1.12"
19
19
  COOKIE_NAME = "prodmaker_session"
20
20
  SESSION_DAYS = 30
21
21
  MASTER_ALIAS = "admin"
@@ -38,6 +38,8 @@ from .api import dashboard as dashboard_router # noqa: E402
38
38
  from .api import pins as pins_router # noqa: E402
39
39
  from .api import logs as logs_router # noqa: E402
40
40
  from .api import guias as guias_router # noqa: E402
41
+ from .api import contas as contas_router # noqa: E402
42
+ from .api import invest as invest_router # noqa: E402
41
43
 
42
44
  app.include_router(llm_router.router)
43
45
  app.include_router(infoprodutos_router.router)
@@ -52,6 +54,8 @@ app.include_router(dashboard_router.router)
52
54
  app.include_router(pins_router.router)
53
55
  app.include_router(logs_router.router)
54
56
  app.include_router(guias_router.router)
57
+ app.include_router(contas_router.router)
58
+ app.include_router(invest_router.router)
55
59
 
56
60
  ensure_db()
57
61
 
@@ -258,7 +262,7 @@ def health():
258
262
 
259
263
  @app.get("/api/version")
260
264
  def version():
261
- return {"version": APP_VERSION, "canonical": "0.1.11"}
265
+ return {"version": APP_VERSION, "canonical": "0.1.12"}
262
266
 
263
267
 
264
268
  @app.get("/api/settings")
@@ -1,218 +1,237 @@
1
- """Banco SQLite local (stdlib) — helpers compartilhados."""
2
- import sqlite3
3
- import time
4
- from pathlib import Path
1
+ """Banco de dados — façade sobre dbengine (SQLite padrão, PostgreSQL/Supabase via DATABASE_URL).
5
2
 
6
- from .store import STATE
3
+ Esquema único para os dois engines. Todas as tabelas carregam `user_id` (preparação
4
+ multi-tenant — hoje vazio). `ensure_db()` cria o que falta e aplica ALTERs de migração.
5
+ """
6
+ from .dbengine import (
7
+ add_column_if_missing,
8
+ is_postgres,
9
+ now_str, # noqa: F401 (reexportado para os serviços)
10
+ row_to_dict, # noqa: F401 (reexportado para os serviços)
11
+ )
7
12
 
8
- DB_PATH = STATE / "prodmaker.db"
13
+ # ---------------------------------------------------------------------------
14
+ # Esquema — SQLite DDL; para PostgreSQL as mesmas definições são traduzidas.
15
+ # ---------------------------------------------------------------------------
9
16
 
10
-
11
- def get_db():
12
- STATE.mkdir(parents=True, exist_ok=True)
13
- conn = sqlite3.connect(DB_PATH)
14
- conn.row_factory = sqlite3.Row
15
- return conn
16
-
17
-
18
- def ensure_db() -> None:
19
- with get_db() as conn:
20
- conn.execute(
21
- """
22
- CREATE TABLE IF NOT EXISTS produtos (
23
- id INTEGER PRIMARY KEY AUTOINCREMENT,
24
- nome TEXT NOT NULL,
25
- plataforma TEXT NOT NULL DEFAULT 'Outro',
26
- link_afiliado TEXT NOT NULL DEFAULT '',
27
- comissao REAL NOT NULL DEFAULT 0,
28
- preco REAL NOT NULL DEFAULT 0,
29
- nicho TEXT NOT NULL DEFAULT '',
30
- status TEXT NOT NULL DEFAULT 'em_analise',
31
- created_at TEXT NOT NULL,
32
- updated_at TEXT NOT NULL
33
- )
34
- """
17
+ TABLES = {
18
+ "produtos": """
19
+ CREATE TABLE IF NOT EXISTS produtos (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ user_id TEXT NOT NULL DEFAULT '',
22
+ nome TEXT NOT NULL,
23
+ plataforma TEXT NOT NULL DEFAULT 'Outro',
24
+ link_afiliado TEXT NOT NULL DEFAULT '',
25
+ comissao REAL NOT NULL DEFAULT 0,
26
+ preco REAL NOT NULL DEFAULT 0,
27
+ nicho TEXT NOT NULL DEFAULT '',
28
+ status TEXT NOT NULL DEFAULT 'em_analise',
29
+ analysis TEXT NOT NULL DEFAULT '',
30
+ created_at TEXT NOT NULL,
31
+ updated_at TEXT NOT NULL
35
32
  )
36
- conn.execute(
37
- """
38
- CREATE TABLE IF NOT EXISTS infoprodutos (
39
- id INTEGER PRIMARY KEY AUTOINCREMENT,
40
- titulo TEXT NOT NULL,
41
- formato TEXT NOT NULL DEFAULT 'E-book',
42
- nicho TEXT NOT NULL DEFAULT '',
43
- modo TEXT NOT NULL DEFAULT 'rapido',
44
- provider TEXT NOT NULL DEFAULT '',
45
- pdf_path TEXT NOT NULL DEFAULT '',
46
- md_path TEXT NOT NULL DEFAULT '',
47
- created_at TEXT NOT NULL
48
- )
49
- """
33
+ """,
34
+ "infoprodutos": """
35
+ CREATE TABLE IF NOT EXISTS infoprodutos (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ user_id TEXT NOT NULL DEFAULT '',
38
+ titulo TEXT NOT NULL,
39
+ formato TEXT NOT NULL DEFAULT 'E-book',
40
+ nicho TEXT NOT NULL DEFAULT '',
41
+ modo TEXT NOT NULL DEFAULT 'rapido',
42
+ provider TEXT NOT NULL DEFAULT '',
43
+ pdf_path TEXT NOT NULL DEFAULT '',
44
+ md_path TEXT NOT NULL DEFAULT '',
45
+ created_at TEXT NOT NULL
50
46
  )
51
- conn.execute(
52
- """
53
- CREATE TABLE IF NOT EXISTS paginas (
54
- id INTEGER PRIMARY KEY AUTOINCREMENT,
55
- titulo TEXT NOT NULL,
56
- tipo TEXT NOT NULL,
57
- produto_id INTEGER,
58
- idioma TEXT NOT NULL DEFAULT 'Portugues',
59
- html_path TEXT NOT NULL DEFAULT '',
60
- status TEXT NOT NULL DEFAULT 'rascunho',
61
- published_url TEXT NOT NULL DEFAULT '',
62
- created_at TEXT NOT NULL,
63
- updated_at TEXT NOT NULL
64
- )
65
- """
47
+ """,
48
+ "paginas": """
49
+ CREATE TABLE IF NOT EXISTS paginas (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ user_id TEXT NOT NULL DEFAULT '',
52
+ titulo TEXT NOT NULL,
53
+ tipo TEXT NOT NULL,
54
+ produto_id INTEGER,
55
+ idioma TEXT NOT NULL DEFAULT 'Portugues',
56
+ html_path TEXT NOT NULL DEFAULT '',
57
+ status TEXT NOT NULL DEFAULT 'rascunho',
58
+ published_url TEXT NOT NULL DEFAULT '',
59
+ created_at TEXT NOT NULL,
60
+ updated_at TEXT NOT NULL
66
61
  )
67
- conn.execute(
68
- """
69
- CREATE TABLE IF NOT EXISTS dominios (
70
- id INTEGER PRIMARY KEY AUTOINCREMENT,
71
- nome TEXT NOT NULL,
72
- status TEXT NOT NULL DEFAULT 'pendente',
73
- cf_zone_id TEXT NOT NULL DEFAULT '',
74
- ns_data TEXT NOT NULL DEFAULT '',
75
- a_data TEXT NOT NULL DEFAULT '',
76
- tags TEXT NOT NULL DEFAULT '',
77
- created_at TEXT NOT NULL,
78
- updated_at TEXT NOT NULL
79
- )
80
- """
62
+ """,
63
+ "dominios": """
64
+ CREATE TABLE IF NOT EXISTS dominios (
65
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
66
+ user_id TEXT NOT NULL DEFAULT '',
67
+ nome TEXT NOT NULL,
68
+ status TEXT NOT NULL DEFAULT 'pendente',
69
+ cf_zone_id TEXT NOT NULL DEFAULT '',
70
+ ns_data TEXT NOT NULL DEFAULT '',
71
+ a_data TEXT NOT NULL DEFAULT '',
72
+ tags TEXT NOT NULL DEFAULT '',
73
+ created_at TEXT NOT NULL,
74
+ updated_at TEXT NOT NULL
81
75
  )
82
- conn.execute(
83
- """
84
- CREATE TABLE IF NOT EXISTS visitas (
85
- id INTEGER PRIMARY KEY AUTOINCREMENT,
86
- event TEXT NOT NULL DEFAULT '',
87
- page TEXT NOT NULL DEFAULT '',
88
- gclid TEXT NOT NULL DEFAULT '',
89
- utm_source TEXT NOT NULL DEFAULT '',
90
- utm_medium TEXT NOT NULL DEFAULT '',
91
- utm_campaign TEXT NOT NULL DEFAULT '',
92
- ref TEXT NOT NULL DEFAULT '',
93
- ip TEXT NOT NULL DEFAULT '',
94
- user_agent TEXT NOT NULL DEFAULT '',
95
- extra TEXT NOT NULL DEFAULT '',
96
- created_at TEXT NOT NULL
97
- )
98
- """
76
+ """,
77
+ "visitas": """
78
+ CREATE TABLE IF NOT EXISTS visitas (
79
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
80
+ user_id TEXT NOT NULL DEFAULT '',
81
+ event TEXT NOT NULL DEFAULT '',
82
+ page TEXT NOT NULL DEFAULT '',
83
+ gclid TEXT NOT NULL DEFAULT '',
84
+ utm_source TEXT NOT NULL DEFAULT '',
85
+ utm_medium TEXT NOT NULL DEFAULT '',
86
+ utm_campaign TEXT NOT NULL DEFAULT '',
87
+ ref TEXT NOT NULL DEFAULT '',
88
+ ip TEXT NOT NULL DEFAULT '',
89
+ user_agent TEXT NOT NULL DEFAULT '',
90
+ extra TEXT NOT NULL DEFAULT '',
91
+ created_at TEXT NOT NULL
99
92
  )
100
- conn.execute(
101
- """
102
- CREATE TABLE IF NOT EXISTS vendas (
103
- id INTEGER PRIMARY KEY AUTOINCREMENT,
104
- fonte TEXT NOT NULL DEFAULT 'manual',
105
- produto_id INTEGER,
106
- produto_nome TEXT NOT NULL DEFAULT '',
107
- transacao_id TEXT NOT NULL DEFAULT '',
108
- comprador_email TEXT NOT NULL DEFAULT '',
109
- valor REAL NOT NULL DEFAULT 0,
110
- comissao REAL NOT NULL DEFAULT 0,
111
- gclid TEXT NOT NULL DEFAULT '',
112
- utm_source TEXT NOT NULL DEFAULT '',
113
- utm_medium TEXT NOT NULL DEFAULT '',
114
- utm_campaign TEXT NOT NULL DEFAULT '',
115
- status TEXT NOT NULL DEFAULT 'aprovada',
116
- conversao_status TEXT NOT NULL DEFAULT 'pendente',
117
- conversao_erro TEXT NOT NULL DEFAULT '',
118
- gclid_match TEXT NOT NULL DEFAULT '',
119
- raw TEXT NOT NULL DEFAULT '',
120
- created_at TEXT NOT NULL
121
- )
122
- """
93
+ """,
94
+ "vendas": """
95
+ CREATE TABLE IF NOT EXISTS vendas (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ user_id TEXT NOT NULL DEFAULT '',
98
+ fonte TEXT NOT NULL DEFAULT 'manual',
99
+ produto_id INTEGER,
100
+ produto_nome TEXT NOT NULL DEFAULT '',
101
+ transacao_id TEXT NOT NULL DEFAULT '',
102
+ comprador_email TEXT NOT NULL DEFAULT '',
103
+ valor REAL NOT NULL DEFAULT 0,
104
+ comissao REAL NOT NULL DEFAULT 0,
105
+ gclid TEXT NOT NULL DEFAULT '',
106
+ utm_source TEXT NOT NULL DEFAULT '',
107
+ utm_medium TEXT NOT NULL DEFAULT '',
108
+ utm_campaign TEXT NOT NULL DEFAULT '',
109
+ status TEXT NOT NULL DEFAULT 'aprovada',
110
+ conversao_status TEXT NOT NULL DEFAULT 'pendente',
111
+ conversao_erro TEXT NOT NULL DEFAULT '',
112
+ gclid_match TEXT NOT NULL DEFAULT '',
113
+ raw TEXT NOT NULL DEFAULT '',
114
+ created_at TEXT NOT NULL
123
115
  )
124
- conn.execute("CREATE INDEX IF NOT EXISTS idx_vendas_gclid ON vendas (gclid)")
125
- conn.execute("CREATE INDEX IF NOT EXISTS idx_vendas_created ON vendas (created_at)")
126
- conn.execute("CREATE INDEX IF NOT EXISTS idx_visitas_gclid ON visitas (gclid)")
127
- conn.execute(
128
- """
129
- CREATE TABLE IF NOT EXISTS palavras (
130
- id INTEGER PRIMARY KEY AUTOINCREMENT,
131
- produto_id INTEGER,
132
- produto_nome TEXT NOT NULL DEFAULT '',
133
- tipo TEXT NOT NULL DEFAULT 'positiva',
134
- texto TEXT NOT NULL,
135
- correspondencia TEXT NOT NULL DEFAULT 'frase',
136
- origem TEXT NOT NULL DEFAULT 'ia',
137
- created_at TEXT NOT NULL
138
- )
139
- """
116
+ """,
117
+ "palavras": """
118
+ CREATE TABLE IF NOT EXISTS palavras (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ user_id TEXT NOT NULL DEFAULT '',
121
+ produto_id INTEGER,
122
+ produto_nome TEXT NOT NULL DEFAULT '',
123
+ tipo TEXT NOT NULL DEFAULT 'positiva',
124
+ texto TEXT NOT NULL,
125
+ correspondencia TEXT NOT NULL DEFAULT 'frase',
126
+ origem TEXT NOT NULL DEFAULT 'ia',
127
+ created_at TEXT NOT NULL
140
128
  )
141
- conn.execute(
142
- """
143
- CREATE TABLE IF NOT EXISTS campanhas (
144
- id INTEGER PRIMARY KEY AUTOINCREMENT,
145
- produto_id INTEGER,
146
- plataforma TEXT NOT NULL DEFAULT 'google',
147
- nome TEXT NOT NULL,
148
- objetivo TEXT NOT NULL DEFAULT '',
149
- cpa_alvo REAL NOT NULL DEFAULT 0,
150
- orcamento_diario REAL NOT NULL DEFAULT 0,
151
- keywords_json TEXT NOT NULL DEFAULT '[]',
152
- negativas_json TEXT NOT NULL DEFAULT '[]',
153
- criativos_json TEXT NOT NULL DEFAULT '[]',
154
- plano_json TEXT NOT NULL DEFAULT '',
155
- status TEXT NOT NULL DEFAULT 'rascunho',
156
- resultado_json TEXT NOT NULL DEFAULT '',
157
- erro TEXT NOT NULL DEFAULT '',
158
- created_at TEXT NOT NULL,
159
- updated_at TEXT NOT NULL
160
- )
161
- """
129
+ """,
130
+ "campanhas": """
131
+ CREATE TABLE IF NOT EXISTS campanhas (
132
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
133
+ user_id TEXT NOT NULL DEFAULT '',
134
+ produto_id INTEGER,
135
+ plataforma TEXT NOT NULL DEFAULT 'google',
136
+ nome TEXT NOT NULL,
137
+ objetivo TEXT NOT NULL DEFAULT '',
138
+ cpa_alvo REAL NOT NULL DEFAULT 0,
139
+ orcamento_diario REAL NOT NULL DEFAULT 0,
140
+ keywords_json TEXT NOT NULL DEFAULT '[]',
141
+ negativas_json TEXT NOT NULL DEFAULT '[]',
142
+ criativos_json TEXT NOT NULL DEFAULT '[]',
143
+ plano_json TEXT NOT NULL DEFAULT '',
144
+ status TEXT NOT NULL DEFAULT 'rascunho',
145
+ resultado_json TEXT NOT NULL DEFAULT '',
146
+ erro TEXT NOT NULL DEFAULT '',
147
+ created_at TEXT NOT NULL,
148
+ updated_at TEXT NOT NULL
162
149
  )
163
- columns = [row["name"] for row in conn.execute("PRAGMA table_info(produtos)")]
164
- if "analysis" not in columns:
165
- conn.execute("ALTER TABLE produtos ADD COLUMN analysis TEXT NOT NULL DEFAULT ''")
166
- conn.execute(
167
- """
168
- CREATE TABLE IF NOT EXISTS lancamentos (
169
- id INTEGER PRIMARY KEY AUTOINCREMENT,
170
- produto_id INTEGER,
171
- etapa_atual INTEGER NOT NULL DEFAULT 1,
172
- status TEXT NOT NULL DEFAULT 'em_andamento',
173
- dados_json TEXT NOT NULL DEFAULT '{}',
174
- created_at TEXT NOT NULL,
175
- updated_at TEXT NOT NULL
176
- )
177
- """
150
+ """,
151
+ "lancamentos": """
152
+ CREATE TABLE IF NOT EXISTS lancamentos (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ user_id TEXT NOT NULL DEFAULT '',
155
+ produto_id INTEGER,
156
+ etapa_atual INTEGER NOT NULL DEFAULT 1,
157
+ status TEXT NOT NULL DEFAULT 'em_andamento',
158
+ dados_json TEXT NOT NULL DEFAULT '{}',
159
+ created_at TEXT NOT NULL,
160
+ updated_at TEXT NOT NULL
178
161
  )
179
- conn.execute(
180
- """
181
- CREATE TABLE IF NOT EXISTS notificacoes (
182
- id INTEGER PRIMARY KEY AUTOINCREMENT,
183
- event TEXT NOT NULL DEFAULT '',
184
- titulo TEXT NOT NULL DEFAULT '',
185
- mensagem TEXT NOT NULL DEFAULT '',
186
- canal TEXT NOT NULL DEFAULT '',
187
- status TEXT NOT NULL DEFAULT 'pendente',
188
- erro TEXT NOT NULL DEFAULT '',
189
- created_at TEXT NOT NULL
190
- )
191
- """
162
+ """,
163
+ "notificacoes": """
164
+ CREATE TABLE IF NOT EXISTS notificacoes (
165
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
166
+ user_id TEXT NOT NULL DEFAULT '',
167
+ event TEXT NOT NULL DEFAULT '',
168
+ titulo TEXT NOT NULL DEFAULT '',
169
+ mensagem TEXT NOT NULL DEFAULT '',
170
+ canal TEXT NOT NULL DEFAULT '',
171
+ status TEXT NOT NULL DEFAULT 'pendente',
172
+ erro TEXT NOT NULL DEFAULT '',
173
+ created_at TEXT NOT NULL
192
174
  )
193
- conn.execute(
194
- """
195
- CREATE TABLE IF NOT EXISTS pins (
196
- id INTEGER PRIMARY KEY AUTOINCREMENT,
197
- produto_id INTEGER,
198
- titulo TEXT NOT NULL,
199
- descricao TEXT NOT NULL DEFAULT '',
200
- texto_imagem TEXT NOT NULL DEFAULT '',
201
- sugestao_imagem TEXT NOT NULL DEFAULT '',
202
- board TEXT NOT NULL DEFAULT '',
203
- link TEXT NOT NULL DEFAULT '',
204
- status TEXT NOT NULL DEFAULT 'ideia',
205
- provider TEXT NOT NULL DEFAULT '',
206
- erro TEXT NOT NULL DEFAULT '',
207
- created_at TEXT NOT NULL
208
- )
209
- """
175
+ """,
176
+ "pins": """
177
+ CREATE TABLE IF NOT EXISTS pins (
178
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
179
+ user_id TEXT NOT NULL DEFAULT '',
180
+ produto_id INTEGER,
181
+ titulo TEXT NOT NULL,
182
+ descricao TEXT NOT NULL DEFAULT '',
183
+ texto_imagem TEXT NOT NULL DEFAULT '',
184
+ sugestao_imagem TEXT NOT NULL DEFAULT '',
185
+ board TEXT NOT NULL DEFAULT '',
186
+ link TEXT NOT NULL DEFAULT '',
187
+ status TEXT NOT NULL DEFAULT 'ideia',
188
+ provider TEXT NOT NULL DEFAULT '',
189
+ erro TEXT NOT NULL DEFAULT '',
190
+ created_at TEXT NOT NULL
210
191
  )
192
+ """,
193
+ "investimentos": """
194
+ CREATE TABLE IF NOT EXISTS investimentos (
195
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
196
+ user_id TEXT NOT NULL DEFAULT '',
197
+ data TEXT NOT NULL DEFAULT '',
198
+ valor REAL NOT NULL DEFAULT 0,
199
+ fonte TEXT NOT NULL DEFAULT 'manual',
200
+ observacao TEXT NOT NULL DEFAULT '',
201
+ created_at TEXT NOT NULL
202
+ )
203
+ """,
204
+ }
205
+
206
+ ALL_TABLES = list(TABLES.keys())
211
207
 
212
208
 
213
- def now_str() -> str:
214
- return time.strftime("%Y-%m-%d %H:%M:%S")
209
+ def _pg_ddl(sqlite_ddl: str) -> str:
210
+ """Traduz o DDL SQLite para PostgreSQL."""
211
+ return sqlite_ddl.replace("INTEGER PRIMARY KEY AUTOINCREMENT", "BIGSERIAL PRIMARY KEY")
212
+
213
+
214
+ def get_db():
215
+ from .dbengine import connect
216
+
217
+ return connect()
218
+
219
+
220
+ def ensure_db() -> None:
221
+ with get_db() as conn:
222
+ if is_postgres():
223
+ for ddl in TABLES.values():
224
+ conn.execute(_pg_ddl(ddl))
225
+ else:
226
+ for ddl in TABLES.values():
227
+ conn.execute(ddl)
228
+ # Migração: bancos criados antes da coluna user_id / da tabela investimentos
229
+ for table in ALL_TABLES:
230
+ add_column_if_missing(conn, table, "user_id TEXT NOT NULL DEFAULT ''", "user_id")
231
+ if "investimentos" not in [row["name"] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")]:
232
+ conn.execute(TABLES["investimentos"])
233
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_vendas_gclid ON vendas (gclid)")
234
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_visitas_gclid ON visitas (gclid)")
215
235
 
216
236
 
217
- def row_to_dict(row) -> dict:
218
- return {key: row[key] for key in row.keys()}
237
+ ensure_db()