@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 +20 -0
- package/app/api/contas.py +45 -0
- package/app/api/invest.py +37 -0
- package/app/main.py +6 -2
- package/app/services/database.py +219 -200
- package/app/services/dbengine.py +195 -0
- package/app/services/invest.py +52 -0
- package/app/services/oauth_google.py +108 -0
- package/frontend/dist/assets/index-C8sX5Mkf.js +74 -0
- package/frontend/dist/index.html +1 -1
- package/package.json +1 -1
- package/requirements.txt +3 -0
- package/scripts/cli.mjs +1 -1
- package/frontend/dist/assets/index-DGhGACYG.js +0 -74
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.
|
|
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.
|
|
265
|
+
return {"version": APP_VERSION, "canonical": "0.1.12"}
|
|
262
266
|
|
|
263
267
|
|
|
264
268
|
@app.get("/api/settings")
|
package/app/services/database.py
CHANGED
|
@@ -1,218 +1,237 @@
|
|
|
1
|
-
"""Banco
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# Esquema — SQLite DDL; para PostgreSQL as mesmas definições são traduzidas.
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
return {key: row[key] for key in row.keys()}
|
|
237
|
+
ensure_db()
|