@danhachuel/prodmaker 0.1.11 → 0.1.13
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 +37 -0
- package/app/api/builder.py +81 -0
- package/app/api/contas.py +45 -0
- package/app/api/invest.py +37 -0
- package/app/main.py +8 -2
- package/app/services/builder.py +217 -0
- 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-dz1c2OPb.js +75 -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/seed/prompts/builder-anuncios.md +20 -0
- package/seed/prompts/builder-estrutura.md +33 -0
- package/seed/prompts/builder-ofertas.md +27 -0
- package/frontend/dist/assets/index-DGhGACYG.js +0 -74
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Investimento em tráfego — base do ROAS real e do Lucro Líquido."""
|
|
2
|
+
from .database import get_db, now_str, row_to_dict
|
|
3
|
+
from .llm import LLMError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def registrar(valor: float, data: str = "", observacao: str = "") -> dict:
|
|
7
|
+
if float(valor) <= 0:
|
|
8
|
+
raise LLMError("Informe um valor de investimento maior que zero.")
|
|
9
|
+
data = (data or "").strip() or now_str()[:10]
|
|
10
|
+
timestamp = now_str()
|
|
11
|
+
with get_db() as conn:
|
|
12
|
+
cursor = conn.execute(
|
|
13
|
+
"INSERT INTO investimentos (data, valor, fonte, observacao, created_at) VALUES (?, ?, 'manual', ?, ?)",
|
|
14
|
+
(data, float(valor), observacao.strip(), timestamp),
|
|
15
|
+
)
|
|
16
|
+
row = conn.execute("SELECT * FROM investimentos WHERE id = ?", (cursor.lastrowid,)).fetchone()
|
|
17
|
+
return row_to_dict(row)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def listar(limit: int = 60) -> list[dict]:
|
|
21
|
+
with get_db() as conn:
|
|
22
|
+
rows = conn.execute("SELECT * FROM investimentos ORDER BY id DESC LIMIT ?", (limit,)).fetchall()
|
|
23
|
+
return [row_to_dict(row) for row in rows]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def remover(investimento_id: int) -> None:
|
|
27
|
+
with get_db() as conn:
|
|
28
|
+
cursor = conn.execute("DELETE FROM investimentos WHERE id = ?", (investimento_id,))
|
|
29
|
+
if cursor.rowcount == 0:
|
|
30
|
+
raise LLMError("Registro não encontrado.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def resumo_financeiro() -> dict:
|
|
34
|
+
"""ROAS e Lucro Líquido reais (faturamento = receita das vendas aprovadas)."""
|
|
35
|
+
with get_db() as conn:
|
|
36
|
+
receita = conn.execute(
|
|
37
|
+
"SELECT COALESCE(SUM(valor),0) AS r, COALESCE(SUM(comissao),0) AS c FROM vendas WHERE status = 'aprovada'"
|
|
38
|
+
).fetchone()
|
|
39
|
+
investido = conn.execute("SELECT COALESCE(SUM(valor),0) AS t FROM investimentos").fetchone()
|
|
40
|
+
receita_valor = float(receita["r"] or 0)
|
|
41
|
+
comissao_valor = float(receita["c"] or 0)
|
|
42
|
+
investido_valor = float(investido["t"] or 0)
|
|
43
|
+
roas = round(receita_valor / investido_valor, 2) if investido_valor > 0 else None
|
|
44
|
+
lucro = round(receita_valor - investido_valor, 2)
|
|
45
|
+
return {
|
|
46
|
+
"faturamento": round(receita_valor, 2),
|
|
47
|
+
"comissao": round(comissao_valor, 2),
|
|
48
|
+
"investimento": round(investido_valor, 2),
|
|
49
|
+
"lucro_liquido": lucro,
|
|
50
|
+
"roas": roas,
|
|
51
|
+
"tem_investimento": investido_valor > 0,
|
|
52
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Google OAuth 2.0 — conexão de conta com 1 clique (gera o Refresh Token automaticmente).
|
|
2
|
+
|
|
3
|
+
Fluxo:
|
|
4
|
+
1. `GET /api/google/oauth/url` monta a URL de consentimento (scopes do Google Ads,
|
|
5
|
+
access_type=offline + prompt=consent para GARANTIR o refresh_token).
|
|
6
|
+
2. O usuário autoriza no popup; o Google redireciona para `redirect_uri`
|
|
7
|
+
(http://localhost:8188/api/google/callback?code=...).
|
|
8
|
+
3. `GET /api/google/callback` troca o code por tokens e salva o refresh_token em
|
|
9
|
+
settings.api_keys.google-ads (status "conectado").
|
|
10
|
+
|
|
11
|
+
Observação: o usuário precisa criar um OAuth Client ID (tipo "Desktop app" ou "Web")
|
|
12
|
+
no Google Cloud, com a URL de redirect autorizada, e colar client_id/secret uma vez
|
|
13
|
+
em Chaves de API. A autorização da conta passa a ser 1 clique.
|
|
14
|
+
"""
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import httpx
|
|
18
|
+
|
|
19
|
+
from .store import get_settings, save_settings
|
|
20
|
+
|
|
21
|
+
OAUTH_SCOPE = "https://www.googleapis.com/auth/adwords"
|
|
22
|
+
TOKEN_URL = "https://oauth2.googleapis.com/token"
|
|
23
|
+
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def callback_url(request=None) -> str:
|
|
27
|
+
base = "http://localhost:8188"
|
|
28
|
+
if request is not None:
|
|
29
|
+
host = request.headers.get("host")
|
|
30
|
+
base = f"{request.url.scheme}://{host}"
|
|
31
|
+
return f"{base}/api/google/callback"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_auth_url(request=None) -> dict:
|
|
35
|
+
keys = (get_settings().get("api_keys") or {}).get("google-ads") or {}
|
|
36
|
+
client_id = (keys.get("client_id") or "").strip()
|
|
37
|
+
if not client_id:
|
|
38
|
+
from .llm import LLMError
|
|
39
|
+
|
|
40
|
+
raise LLMError(
|
|
41
|
+
"Para conectar com 1 clique, primeiro salve o 'OAuth Client ID' do Google Cloud em Chaves de API (card Google Ads)."
|
|
42
|
+
)
|
|
43
|
+
redirect = callback_url(request)
|
|
44
|
+
import urllib.parse
|
|
45
|
+
|
|
46
|
+
query = urllib.parse.urlencode(
|
|
47
|
+
{
|
|
48
|
+
"client_id": client_id,
|
|
49
|
+
"redirect_uri": redirect,
|
|
50
|
+
"response_type": "code",
|
|
51
|
+
"scope": OAUTH_SCOPE,
|
|
52
|
+
"access_type": "offline",
|
|
53
|
+
"prompt": "consent", # força a devolução do refresh_token
|
|
54
|
+
"include_granted_scopes": "true",
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
return {"url": f"{AUTH_URL}?{query}", "redirect_uri": redirect}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def exchange_code(code: str, request=None) -> dict:
|
|
61
|
+
keys = (get_settings().get("api_keys") or {}).get("google-ads") or {}
|
|
62
|
+
client_id = (keys.get("client_id") or "").strip()
|
|
63
|
+
client_secret = (keys.get("client_secret") or "").strip()
|
|
64
|
+
if not client_id or not client_secret:
|
|
65
|
+
from .llm import LLMError
|
|
66
|
+
|
|
67
|
+
raise LLMError("client_id e client_secret precisam estar salvos em Chaves de API antes de concluir o OAuth.")
|
|
68
|
+
redirect = callback_url(request)
|
|
69
|
+
try:
|
|
70
|
+
response = httpx.post(
|
|
71
|
+
TOKEN_URL,
|
|
72
|
+
data={
|
|
73
|
+
"client_id": client_id,
|
|
74
|
+
"client_secret": client_secret,
|
|
75
|
+
"code": code.strip(),
|
|
76
|
+
"grant_type": "authorization_code",
|
|
77
|
+
"redirect_uri": redirect,
|
|
78
|
+
},
|
|
79
|
+
timeout=30.0,
|
|
80
|
+
)
|
|
81
|
+
except httpx.HTTPError as exc:
|
|
82
|
+
from .llm import LLMError
|
|
83
|
+
|
|
84
|
+
raise LLMError(f"Falha de conexão com o OAuth do Google: {exc}", provider="google-ads", transient=True) from exc
|
|
85
|
+
if response.status_code != 200:
|
|
86
|
+
from .llm import LLMError
|
|
87
|
+
|
|
88
|
+
raise LLMError(
|
|
89
|
+
f"Google rejeitou a troca do código ({response.status_code}): {response.text[:200]}. "
|
|
90
|
+
"Confira redirect_uri autorizado e tente novamente.",
|
|
91
|
+
provider="google-ads",
|
|
92
|
+
)
|
|
93
|
+
tokens = response.json() or {}
|
|
94
|
+
refresh_token = tokens.get("refresh_token")
|
|
95
|
+
if not refresh_token:
|
|
96
|
+
from .llm import LLMError
|
|
97
|
+
|
|
98
|
+
raise LLMError(
|
|
99
|
+
"O Google não devolveu refresh_token — apague o app nas configurações da conta Google "
|
|
100
|
+
"(apps de terceiros conectados) e tente novamente, ou marque prompt=consent."
|
|
101
|
+
)
|
|
102
|
+
settings = get_settings()
|
|
103
|
+
api_keys = settings.setdefault("api_keys", {})
|
|
104
|
+
google = api_keys.setdefault("google-ads", {})
|
|
105
|
+
google["refresh_token"] = refresh_token
|
|
106
|
+
google["connected_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
|
107
|
+
save_settings(settings)
|
|
108
|
+
return {"ok": True, "connected_at": google["connected_at"], "scope": tokens.get("scope", OAUTH_SCOPE)}
|