@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 CHANGED
@@ -3,6 +3,43 @@
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.13 — 2026-10-31
7
+
8
+ - **Fase 10 — Product Builder Wizard (motor de criação de ofertas).**
9
+ - Studio substituído pelo **Wizard de 3 passos**: (1) mineração de **5 ofertas validadas**
10
+ a partir do nicho (promessa, dores, entregáveis, bônus, preço R$27–97) + registro do
11
+ **Parceiro 50/50**; (2) **estrutura** (módulos com aulas, e-books, ferramentas, **Quiz
12
+ com IA** e **3 Order Bumps** — checkout +R$21,90, upsell +R$197, parceiro 50/50) +
13
+ precificação com trava de faixa; (3) **12 anúncios** (Prova social/material/lógica,
14
+ Desafio-garantia, Comparação isso-vs-aquilo) com cópia por anúncio + **exportação de PDF**
15
+ (Markdown → HTML A4 → PDF) e **prompts de imagem** para Gamma/Canva.
16
+ - Backend: `services/builder.py` + `api/builder.py` (`POST /api/builder/generate-offers`,
17
+ `/generate-structure`, `/generate-ads`, `/generate-pdf`, `/salvar`). Tudo é salvo em
18
+ `infoprodutos` (novas colunas `oferta_json`, `estrutura_json`, `anuncios_json`, `preco`,
19
+ `parceiro` com migração automática) para AdsPages/AdsCreator consumirem.
20
+ - Novos prompts no seed: `builder-ofertas.md`, `builder-estrutura.md`, `builder-anuncios.md`.
21
+ - Rota `/studio` agora abre o Wizard; barra de progresso com "passo X de 3".
22
+
23
+ ## 0.1.12 — 2026-10-31
24
+
25
+ - **Parte 10 — Polish E2E + OAuth Google 1 clique + radar Supabase + ROAS real.**
26
+ - **OAuth Google (Contas do Google real):** `GET /api/google/oauth/url` monta o consentimento
27
+ (scope adwords, access_type=offline, prompt=consent) e `GET /api/google/callback` troca o
28
+ código e **salva o refresh_token automaticamente** em Chaves de API. Página "Contas do
29
+ Google" com status de conexão, botão conectar/reconectar (popup) e instruções do redirect
30
+ `http://localhost:8188/api/google/callback`. Basta colar Client ID/Secret uma vez.
31
+ - **Radar Supabase (camada pronta, multi-tenant NÃO ativado):** `dbengine.py` abstrai o banco —
32
+ SQLite continua o padrão; definindo `DATABASE_URL` (PostgreSQL/Supabase) o sistema conecta
33
+ via psycopg (dependência declarada, import preguiçoso), com tradução de placeholders `?`→`%s`
34
+ e linhas dict (mesmo acesso por coluna nos serviços). Todas as 12 tabelas passaram a ter
35
+ `user_id` (migração ALTER aplicada automaticamente nos bancos existentes) — base para o
36
+ isolamento multi-tenant via Supabase Auth na próxima etapa.
37
+ - **ROAS real:** tabela `investimentos` (gasto diário em tráfego) + registro/remoção no
38
+ Dashboard; cards **Investimento**, **ROAS** (Receita ÷ Investimento) e **Lucro Líquido**
39
+ (Receita − Investimento) calculados com dados reais — sem investimento registrado, ROAS
40
+ mostra "—" (nunca inventado).
41
+ - **Contas do Google** saiu do placeholder — menu 100% implementado.
42
+
6
43
  ## 0.1.11 — 2026-10-30
7
44
 
8
45
  - **Parte 9 — Pinterest (orgânico), Logs unificado e FAQ/Documentação** — fecha o menu do AdsX.
@@ -0,0 +1,81 @@
1
+ """Product Builder Wizard — 4 rotas de LLM + geração de PDF (Fase 10)."""
2
+ from fastapi import APIRouter, HTTPException
3
+ from pydantic import BaseModel
4
+
5
+ from ..services import builder
6
+ from ..services.llm import LLMError
7
+
8
+ router = APIRouter(prefix="/api/builder", tags=["builder"])
9
+
10
+
11
+ class OfertasBody(BaseModel):
12
+ nicho: str
13
+ avatar: str = ""
14
+
15
+
16
+ class EstruturaBody(BaseModel):
17
+ oferta: dict
18
+
19
+
20
+ class AnunciosBody(BaseModel):
21
+ produto: str
22
+ promessa: str = ""
23
+ nicho: str = ""
24
+ link: str = ""
25
+
26
+
27
+ class PdfBody(BaseModel):
28
+ oferta: dict
29
+ estrutura: dict
30
+ preco: float
31
+ parceiro: str = ""
32
+ modo: str = "rapido"
33
+
34
+
35
+ class SalvarBody(BaseModel):
36
+ record_id: int
37
+ anuncios: list
38
+
39
+
40
+ def _erro(exc: LLMError) -> HTTPException:
41
+ return HTTPException(status_code=400, detail=str(exc))
42
+
43
+
44
+ @router.post("/generate-offers")
45
+ def generate_offers(body: OfertasBody):
46
+ try:
47
+ return builder.gerar_ofertas(body.nicho, body.avatar)
48
+ except LLMError as exc:
49
+ raise _erro(exc)
50
+
51
+
52
+ @router.post("/generate-structure")
53
+ def generate_structure(body: EstruturaBody):
54
+ try:
55
+ return builder.gerar_estrutura(body.oferta)
56
+ except LLMError as exc:
57
+ raise _erro(exc)
58
+
59
+
60
+ @router.post("/generate-ads")
61
+ def generate_ads(body: AnunciosBody):
62
+ try:
63
+ return builder.gerar_anuncios(body.produto, body.promessa, body.nicho, body.link)
64
+ except LLMError as exc:
65
+ raise _erro(exc)
66
+
67
+
68
+ @router.post("/generate-pdf")
69
+ def generate_pdf(body: PdfBody):
70
+ try:
71
+ return builder.gerar_pdf(body.oferta, body.estrutura, body.preco, body.parceiro, body.modo)
72
+ except LLMError as exc:
73
+ raise _erro(exc)
74
+
75
+
76
+ @router.post("/salvar")
77
+ def salvar(body: SalvarBody):
78
+ try:
79
+ return builder.salvar(body.anuncios, body.record_id)
80
+ except (LLMError, ValueError) as exc:
81
+ raise HTTPException(status_code=400, detail=str(exc))
@@ -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.13"
19
19
  COOKIE_NAME = "prodmaker_session"
20
20
  SESSION_DAYS = 30
21
21
  MASTER_ALIAS = "admin"
@@ -38,6 +38,9 @@ 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
43
+ from .api import builder as builder_router # noqa: E402
41
44
 
42
45
  app.include_router(llm_router.router)
43
46
  app.include_router(infoprodutos_router.router)
@@ -52,6 +55,9 @@ app.include_router(dashboard_router.router)
52
55
  app.include_router(pins_router.router)
53
56
  app.include_router(logs_router.router)
54
57
  app.include_router(guias_router.router)
58
+ app.include_router(contas_router.router)
59
+ app.include_router(invest_router.router)
60
+ app.include_router(builder_router.router)
55
61
 
56
62
  ensure_db()
57
63
 
@@ -258,7 +264,7 @@ def health():
258
264
 
259
265
  @app.get("/api/version")
260
266
  def version():
261
- return {"version": APP_VERSION, "canonical": "0.1.11"}
267
+ return {"version": APP_VERSION, "canonical": "0.1.13"}
262
268
 
263
269
 
264
270
  @app.get("/api/settings")
@@ -0,0 +1,217 @@
1
+ """Product Builder Wizard — motor de criação de ofertas (Fase 10).
2
+
3
+ Fluxo: 5 ofertas (nicho) -> estrutura + quiz + 3 order bumps -> 12 anuncios ->
4
+ PDF (Markdown -> HTML -> PDF) -> salva em infoprodutos + produto (AdsPages/AdsCreator
5
+ consomem automaticamente).
6
+ """
7
+ import json
8
+ import re
9
+ from pathlib import Path
10
+
11
+ from .database import get_db, now_str, row_to_dict
12
+ from .llm import LLMError, generate
13
+ from .pdf import markdown_para_pdf
14
+ from .store import STORAGE
15
+
16
+ SEED_DIR = Path(__file__).resolve().parent.parent.parent / "seed"
17
+
18
+
19
+ def _template(nome: str, fallback: str) -> str:
20
+ path = SEED_DIR / "prompts" / nome
21
+ return path.read_text(encoding="utf-8") if path.exists() else fallback
22
+
23
+
24
+ def _fill(template: str, values: dict) -> str:
25
+ for key, value in values.items():
26
+ template = template.replace("{" + key + "}", str(value or ""))
27
+ return template
28
+
29
+
30
+ def _parse_json(text: str) -> dict:
31
+ cleaned = re.sub(r"```(?:json)?", "", text).strip()
32
+ match = re.search(r"\{.*\}", cleaned, re.DOTALL)
33
+ if not match:
34
+ raise LLMError("A resposta do LLM não contém JSON válido.")
35
+ return json.loads(match.group(0))
36
+
37
+
38
+ def _limit_ofertas(ofertas: list) -> list:
39
+ normalizadas = []
40
+ for item in ofertas[:5]:
41
+ if not isinstance(item, dict) or not (item.get("promessa") or "").strip():
42
+ continue
43
+ normalizadas.append({
44
+ "nome": (item.get("nome") or "Oferta").strip()[:80],
45
+ "promessa": (item.get("promessa") or "").strip(),
46
+ "dores": [str(item) for item in (item.get("dores") or [])][:6],
47
+ "entregaveis": [str(item) for item in (item.get("entregaveis") or [])][:8],
48
+ "bonus": [str(item) for item in (item.get("bonus") or [])][:4],
49
+ "preco_sugerido": float(item.get("preco_sugerido") or 47),
50
+ "formato": (item.get("formato") or "E-book").strip(),
51
+ "publico": (item.get("publico") or "").strip(),
52
+ })
53
+ return normalizadas
54
+
55
+
56
+ def gerar_ofertas(nicho: str, avatar: str = "") -> dict:
57
+ nicho = (nicho or "").strip()
58
+ if not nicho:
59
+ raise LLMError("Informe o nicho para gerar as ofertas.")
60
+ prompt = _fill(
61
+ _template("builder-ofertas.md", "Gere 5 ofertas low-ticket para o nicho {NICHO} em JSON com chave 'ofertas'."),
62
+ {"NICHO": nicho, "AVATAR": avatar or "empreendedores e afiliados do nicho"},
63
+ )
64
+ result = generate(prompt, system="Você é um estrategista de ofertas low-ticket. Responda SOMENTE JSON válido.", max_tokens=4000)
65
+ try:
66
+ parsed = _parse_json(result["text"])
67
+ except json.JSONDecodeError as exc:
68
+ raise LLMError(f"JSON do LLM inválido: {exc}") from exc
69
+ ofertas = _limit_ofertas(parsed.get("ofertas") or [])
70
+ if not ofertas:
71
+ raise LLMError("O LLM não retornou ofertas utilizáveis — tente novamente.")
72
+ return {"ofertas": ofertas, "provider": result["provider"]}
73
+
74
+
75
+ def gerar_estrutura(oferta: dict) -> dict:
76
+ if not oferta or not (oferta.get("promessa") or "").strip():
77
+ raise LLMError("Selecione uma oferta antes de gerar a estrutura.")
78
+ prompt = _fill(
79
+ _template("builder-estrutura.md", "Gere a estrutura do produto para {OFERTA} em JSON."),
80
+ {
81
+ "OFERTA": oferta.get("nome") or "",
82
+ "PROMESSA": oferta.get("promessa") or "",
83
+ "NICHO": oferta.get("publico") or (oferta.get("dores") or [""])[0],
84
+ "AVATAR": oferta.get("publico") or "",
85
+ },
86
+ )
87
+ result = generate(prompt, system="Você é um especialista em produtos digitais. Responda SOMENTE JSON válido.", max_tokens=6000)
88
+ try:
89
+ estrutura = _parse_json(result["text"])
90
+ except json.JSONDecodeError as exc:
91
+ raise LLMError(f"JSON do LLM inválido: {exc}") from exc
92
+ if not estrutura.get("modulos"):
93
+ raise LLMError("O LLM não retornou módulos — tente novamente.")
94
+ return {"estrutura": estrutura, "provider": result["provider"]}
95
+
96
+
97
+ def gerar_anuncios(produto: str, promessa: str, nicho: str = "", link: str = "") -> dict:
98
+ if not produto.strip():
99
+ raise LLMError("Informe o nome do produto para gerar os anúncios.")
100
+ prompt = _fill(
101
+ _template("builder-anuncios.md", "Gere 12 anuncios para {PRODUTO} em JSON com chave 'anuncios'."),
102
+ {"PRODUTO": produto, "PROMESSA": promessa, "NICHO": nicho, "LINK": link},
103
+ )
104
+ result = generate(prompt, system="Você é um copywriter de direct response. Responda SOMENTE JSON válido.", max_tokens=5000)
105
+ try:
106
+ parsed = _parse_json(result["text"])
107
+ except json.JSONDecodeError as exc:
108
+ raise LLMError(f"JSON do LLM inválido: {exc}") from exc
109
+ anuncios = [item for item in (parsed.get("anuncios") or []) if isinstance(item, dict)]
110
+ if len(anuncios) < 12:
111
+ raise LLMError(f"O LLM retornou {len(anuncios)} anúncios (esperado 12) — tente novamente.")
112
+ return {"anuncios": anuncios[:12], "provider": result["provider"]}
113
+
114
+
115
+ def _imagem_prompts(oferta: dict, estrutura: dict) -> list[str]:
116
+ prompts = [
117
+ f"Capa de e-book high-conversion para '{oferta.get('nome')}' ({oferta.get('formato')}), promessa: {oferta.get('promessa')}",
118
+ f"Mockup 3D do produto '{oferta.get('nome')}' para página de vendas, fundo claro, estilo premium",
119
+ ]
120
+ for bonus in (oferta.get("bonus") or [])[:2]:
121
+ prompts.append(f"Capa do bônus '{bonus}', estilo coeso com a capa principal")
122
+ for modulo in (estrutura.get("modulos") or [])[:3]:
123
+ prompts.append(f"Imagem de módulo: {modulo.get('titulo')}")
124
+ return prompts
125
+
126
+
127
+ def _markdown_produto(oferta: dict, estrutura: dict, preco: float, parceiro: str) -> str:
128
+ linhas = [f"# {oferta.get('nome') or 'Produto'}", "", f"**Promessa:** {oferta.get('promessa')}", ""]
129
+ if oferta.get("publico"):
130
+ linhas += [f"**Público:** {oferta['publico']}", ""]
131
+ linhas += ["## Módulos", ""]
132
+ for modulo in estrutura.get("modulos") or []:
133
+ linhas.append(f"### {modulo.get('titulo')}")
134
+ for aula in modulo.get("aulas") or []:
135
+ linhas.append(f"- {aula}")
136
+ linhas.append("")
137
+ if estrutura.get("ebooks"):
138
+ linhas += ["## E-books inclusos", ""] + [f"- {item}" for item in estrutura["ebooks"]] + [""]
139
+ if estrutura.get("ferramentas"):
140
+ linhas += ["## Ferramentas e planilhas", ""] + [f"- {item}" for item in estrutura["ferramentas"]] + [""]
141
+ linhas += ["## Order Bumps", ""]
142
+ for bump in estrutura.get("order_bumps") or []:
143
+ linhas.append(f"- **{bump.get('tipo')}**: {bump.get('nome')} — {bump.get('descricao')}")
144
+ linhas += ["", f"## Preço", "", f"**R$ {preco:.2f}**", ""]
145
+ if parceiro:
146
+ linhas += [f"## Parceiro 50/50", "", parceiro, ""]
147
+ return "\n".join(linhas)
148
+
149
+
150
+ def _ensure_columns() -> None:
151
+ from .dbengine import add_column_if_missing
152
+
153
+ with get_db() as conn:
154
+ add_column_if_missing(conn, "infoprodutos", "oferta_json TEXT NOT NULL DEFAULT ''", "oferta_json")
155
+ add_column_if_missing(conn, "infoprodutos", "estrutura_json TEXT NOT NULL DEFAULT ''", "estrutura_json")
156
+ add_column_if_missing(conn, "infoprodutos", "anuncios_json TEXT NOT NULL DEFAULT ''", "anuncios_json")
157
+ add_column_if_missing(conn, "infoprodutos", "preco REAL NOT NULL DEFAULT 0", "preco")
158
+ add_column_if_missing(conn, "infoprodutos", "parceiro TEXT NOT NULL DEFAULT ''", "parceiro")
159
+
160
+
161
+ def gerar_pdf(oferta: dict, estrutura: dict, preco: float, parceiro: str = "", modo: str = "rapido") -> dict:
162
+ """Gera o PDF do produto + prompts de imagem (Gamma/Canva). Salva em infoprodutos."""
163
+ _ensure_columns()
164
+ md_text = _markdown_produto(oferta, estrutura, preco, parceiro)
165
+ prompt_imagens = _imagem_prompts(oferta, estrutura)
166
+ timestamp = now_str()
167
+ with get_db() as conn:
168
+ cursor = conn.execute(
169
+ "INSERT INTO infoprodutos (titulo, formato, nicho, modo, provider, created_at) VALUES (?, ?, ?, '', 'builder', ?)",
170
+ ((oferta.get("nome") or "Produto")[:120], oferta.get("formato") or "E-book", (oferta.get("publico") or "")[:80], timestamp),
171
+ )
172
+ record_id = cursor.lastrowid
173
+
174
+ pasta = STORAGE / "produtos" / str(record_id)
175
+ pasta.mkdir(parents=True, exist_ok=True)
176
+ md_path = pasta / "infoproduto.md"
177
+ md_path.write_text(md_text, encoding="utf-8")
178
+ pdf_path = None
179
+ erro_pdf = ""
180
+ if modo != "somente_markdown":
181
+ try:
182
+ pdf_path = markdown_para_pdf(md_text, pasta / "infoproduto.pdf")
183
+ except RuntimeError as exc:
184
+ erro_pdf = str(exc)
185
+
186
+ with get_db() as conn:
187
+ conn.execute(
188
+ "UPDATE infoprodutos SET oferta_json = ?, estrutura_json = ?, preco = ?, parceiro = ?, md_path = ?, pdf_path = ? WHERE id = ?",
189
+ (
190
+ json.dumps(oferta, ensure_ascii=False),
191
+ json.dumps(estrutura, ensure_ascii=False),
192
+ float(preco or 0),
193
+ parceiro or "",
194
+ md_path.relative_to(STORAGE).as_posix(),
195
+ pdf_path.relative_to(STORAGE).as_posix() if pdf_path else "",
196
+ record_id,
197
+ ),
198
+ )
199
+ row = conn.execute("SELECT * FROM infoprodutos WHERE id = ?", (record_id,)).fetchone()
200
+ record = row_to_dict(row)
201
+ record["markdown"] = md_text
202
+ record["imagem_prompts"] = prompt_imagens
203
+ record["erro_pdf"] = erro_pdf
204
+ return record
205
+
206
+
207
+ def salvar(anuncios: list, record_id: int) -> dict:
208
+ _ensure_columns()
209
+ with get_db() as conn:
210
+ conn.execute(
211
+ "UPDATE infoprodutos SET anuncios_json = ? WHERE id = ?",
212
+ (json.dumps(anuncios[:12], ensure_ascii=False), record_id),
213
+ )
214
+ row = conn.execute("SELECT * FROM infoprodutos WHERE id = ?", (record_id,)).fetchone()
215
+ record = row_to_dict(row)
216
+ oferta = json.loads(record.get("oferta_json") or "{}")
217
+ return {**record, "oferta": oferta}