1const api_cursos = helper_api_cursos.api_cursos; 2 3// A API responde com "Cache-Control: public, max-age=31536000, immutable" e sem 4// "Vary: Authorization". Como o cache do navegador é chaveado só por método + URL, 5// a resposta de uma usuária seria reaproveitada para a próxima que logasse no mesmo 6// navegador. Por isso todo GET autenticado abaixo vai com cache: "no-store". 7const vAPI_cursos = { 8 get_perfil: async (user_token) => { 9 const api_url = `${api_cursos}/perfil`; 10 const headers = new Headers({ 11 Authorization: `Bearer ${user_token}`, 12 }); 13 14 const response = await fetch(api_url, { headers: headers, cache: "no-store" }); 15 16 let perfil = {}; 17 if (response && response.ok && response.status == 200) { 18 perfil = await response.json(); 19 } 20 //console.log(perfil); 21 return perfil; 22 }, 23 update_perfil: async (user_token, perfil) => { 24 const api_url = `${api_cursos}/perfil`; 25 const headers = new Headers({ 26 Accept: "*/*", 27 "Content-Type": "application/json", 28 Authorization: `Bearer ${user_token}`, 29 }); 30 31 const response = await fetch(api_url, { 32 headers: headers, 33 method: "PUT", 34 body: JSON.stringify({ perfil: perfil }), 35 }).catch((error) => { 36 //console.error(error); 37 return error; 38 }); 39 40 return !!(response && response.ok && response.status == 200); 41 }, 42 get_progresso: async (user_token) => { 43 const api_url = `${api_cursos}/progresso`; 44 const headers = new Headers({ 45 Authorization: `Bearer ${user_token}`, 46 }); 47 48 const response = await fetch(api_url, { headers: headers, cache: "no-store" }); 49 50 let progresso = []; 51 if (response && response.ok && response.status == 200) { 52 progresso = await response.json(); 53 } 54 return progresso; 55 }, 56 /** 57 * Grava progresso e devolve o desfecho da chamada, não só o corpo. 58 * 59 * O código HTTP importa para quem precisa decidir se vale tentar de novo. Em 60 * especial o 404: a API responde "Curso não localizado." quando o curso 61 * existe no WordPress mas nunca foi criado lá â nenhuma nova tentativa vai 62 * mudar isso, e insistir só rende um botão que promete o que não cumpre. 63 * 64 * @param {string} user_token 65 * @param {Number|String} conteudo - Id da aula, ou do curso na matrÃcula. 66 * @param {Number|String} categoria_id - term_id do projeto. 67 * @param {Number|String} curso_id 68 * @returns {Promise<{ok: boolean, status: number, dados: *}>} status 0 = rede fora. 69 */ 70 update_progresso_detalhado: async (user_token, conteudo, categoria_id, curso_id) => { 71 const api_url = `${api_cursos}/progresso`; 72 const headers = new Headers({ 73 Accept: "*/*", 74 "Content-Type": "application/json", 75 Authorization: `Bearer ${user_token}`, 76 }); 77 78 let response; 79 try { 80 response = await fetch(api_url, { 81 headers: headers, 82 method: "PUT", 83 body: JSON.stringify({ 84 conteudo: conteudo, 85 categoria: categoria_id, 86 curso: curso_id, 87 }), 88 }); 89 } catch (erro) { 90 // Rede fora: status 0 para o chamador saber que vale tentar depois. 91 console.error(erro); 92 return { ok: false, status: 0, dados: null }; 93 } 94 95 let dados = null; 96 try { 97 dados = await response.json(); 98 } catch (e) { 99 dados = null; 100 } 101 102 // A API confirma devolvendo o curso gravado. Sem isso no corpo, não dá 103 // para afirmar que a matrÃcula existe. 104 const ok = !!( 105 200 === response.status && 106 dados && 107 String(dados.curso) === String(curso_id) 108 ); 109 110 return { ok: ok, status: response.status, dados: dados }; 111 }, 112 update_progresso: async (user_token, conteudo, categoria_id, curso_id) => { 113 const resultado = await vAPI_cursos.update_progresso_detalhado( 114 user_token, 115 conteudo, 116 categoria_id, 117 curso_id 118 ); 119 // Contrato de sempre: o corpo em 200, lista vazia em qualquer outro caso. 120 return 200 === resultado.status && resultado.dados ? resultado.dados : []; 121 }, 122 get_links_aulas: async (curso_id) => { 123 let aulas = []; 124 if (curso_id) { 125 const api_url = `${helper_api_cursos.url}curso/get_links_aulas/${curso_id}`; 126 const response = await fetch(api_url).catch((error) => { 127 console.error(error); 128 }); 129 if (response && response.ok && response.status == 200) { 130 aulas = await response.json(); 131 } 132 } 133 return aulas; 134 }, 135 get_cursos: async (ids) => { 136 let cursos = []; 137 if (ids && ids.length) { 138 const api_url = `${ 139 helper_api_cursos.url 140 }curso/get_cursos?cursos=${ids.join(",")}`; 141 const response = await fetch(api_url).catch((error) => { 142 console.error(error); 143 }); 144 if (response && response.ok && response.status == 200) { 145 try { cursos = await response.json(); } catch(e) {} 146 } 147 } 148 return cursos; 149 }, 150 /** 151 * Trilha de cursos de um programa, para montar os cards. 152 * 153 * @param {Number|String} programa term_id da taxonomia 'projeto'. 154 * @param {Number|String} [curso_id] Curso a tirar da lista â normalmente o que 155 * está sendo visto, que não entra na própria trilha. 156 * @param {String} [user_token] Token da usuária. Com ele cada curso vem com 157 * 'progresso'; sem ele, 'progresso' vem null. 158 * @returns {Promise<Array>} 159 */ 160 get_cursos_programa: async (programa, curso_id, user_token) => { 161 let cursos = []; 162 if (!programa) return cursos; 163 164 let api_url = `${helper_api_cursos.url}curso/get_cursos_programa/?programa=${encodeURIComponent(programa)}`; 165 if (curso_id) api_url += `&curso_id=${encodeURIComponent(curso_id)}`; 166 167 // A rota é fechada para fora: sem o nonce do site ela responde 403. 168 const headers = new Headers({ "X-WP-Nonce": helper_api_cursos.nonce }); 169 // Header com underscore ("user_token") é descartado antes de chegar ao 170 // PHP: a rota recebia a requisição sem token e devolvia progresso nulo, 171 // então nenhum card virava "Ver aulas". A forma com hÃfen chega. 172 if (user_token) headers.append("X-User-Token", user_token); 173 174 // Sem no-store, a resposta de uma usuária seria reaproveitada para a 175 // próxima que logasse no mesmo navegador â o progresso vem junto. 176 const response = await fetch(api_url, { headers: headers, cache: "no-store" }).catch((error) => { 177 console.error(error); 178 }); 179 180 if (response && response.ok && response.status == 200) { 181 try { cursos = await response.json(); } catch (e) {} 182 } 183 return cursos; 184 }, 185 /** 186 * Matricula a usuária em vários cursos de uma vez. 187 * 188 * Um curso por vez, de propósito: a API de progresso é chamada uma vez por 189 * matrÃcula e disparar tudo em paralelo só troca a espera por risco de 190 * throttling. Falha em um curso não derruba os outros. 191 * 192 * O retorno separa três desfechos, porque eles pedem respostas diferentes 193 * na tela: 194 * matriculados - entraram 195 * falhas - deu errado agora; nova tentativa pode funcionar 196 * indisponiveis - a API não conhece o curso (404 "Curso não localizado"). 197 * Curso que existe no WordPress mas nunca foi criado lá; 198 * oferecer "tente novamente" seria mentira. 199 * 200 * @param {string} user_token - Token do Firebase. 201 * @param {Array<Object>} cursos - Cursos a matricular; cada um precisa de `id`. 202 * @param {Number|String} programa - term_id do projeto, gravado no progresso. 203 * @returns {Promise<{matriculados: Array, falhas: Array, indisponiveis: Array}>} 204 */ 205 matricular_em_lote: async (user_token, cursos, programa) => { 206 const matriculados = []; 207 const falhas = []; 208 const indisponiveis = []; 209 210 if (!user_token || !Array.isArray(cursos)) { 211 return { matriculados, falhas, indisponiveis }; 212 } 213 214 for (const curso of cursos) { 215 if (!curso || !curso.id) continue; 216 217 let resultado; 218 try { 219 resultado = await vAPI_cursos.update_progresso_detalhado( 220 user_token, 221 curso.id, 222 programa, 223 curso.id 224 ); 225 } catch (e) { 226 // Nada aqui pode derrubar o lote inteiro: o curso vira falha e 227 // a fila continua. 228 console.warn("[lote] matrÃcula não concluÃda:", e); 229 resultado = { ok: false, status: 0, dados: null }; 230 } 231 232 if (resultado.ok) { 233 matriculados.push(curso); 234 } else if (404 === resultado.status) { 235 indisponiveis.push(curso); 236 } else { 237 falhas.push(curso); 238 } 239 } 240 241 return { matriculados, falhas, indisponiveis }; 242 }, 243 /** 244 * Registra no histórico do HubSpot as matrÃculas feitas em lote. 245 * 246 * Pensado para rodar depois de a tela já ter respondido: são três requisições 247 * por curso (trava de duplicidade, contato e nota), e segurar o botão por elas 248 * faria a aluna esperar por algo que não é a matrÃcula dela. Falha aqui não 249 * desfaz nada â a matrÃcula na API de Cursos já aconteceu. 250 * 251 * @param {Object} opcoes 252 * @param {string} opcoes.access_token - Token do Firebase. 253 * @param {Object} opcoes.user - Usuária logada. 254 * @param {Array<Object>} opcoes.cursos - Cursos efetivamente matriculados. 255 * @returns {Promise<void>} 256 */
257 registrar_matriculas_hubspot: async (opcoes) => { 258 if (!opcoes) return; 259 const { access_token, user, cursos } = opcoes; 260 if (!access_token || !user || !Array.isArray(cursos) || !cursos.length) return; 261 262 // Um perfil só para o lote inteiro: ele é o mesmo em todos os eventos. 263 let perfil = null; 264 try { 265 perfil = await vAPI_cursos.get_perfil(access_token); 266 } catch (e) { 267 return; 268 } 269 if (!perfil || !perfil.email) return; 270 271 for (const curso of cursos) { 272 // Zeros ficam de fora: curso presencial não tem módulo nem aula, e a 273 // linha "Módulos: 0" só poluiria a nota. 274 const detalhes = {}; 275 if (Number(curso.qtd_modulos) > 0) detalhes.qtd_modulos = curso.qtd_modulos; 276 if (Number(curso.qtd_aulas) > 0) detalhes.qtd_aulas = curso.qtd_aulas; 277 278 try { 279 await vAPI_cursos.send_evento_hubspot({ 280 access_token: access_token, 281 user: user, 282 curso: curso, 283 perfil: perfil, 284 evento: "MatrÃcula Curso", 285 detalhes: detalhes, 286 // Zera o progresso: as propriedades descrevem o curso 287 // recém-matriculado, em que ela ainda não começou. 288 propriedades: { progresso_do_curso: 0 }, 289 }); 290 } catch (e) { 291 console.warn("[lote] evento não registrado no HubSpot:", e); 292 } 293 } 294 }, 295 /** 296 * Link da primeira aula do curso, na ordem em que ela é apresentada. 297 * Curso presencial devolve o link único da página de aula presencial. 298 * 299 * @param {Number|String} curso_id 300 * @returns {Promise<string|false>} 301 */ 302 get_link_primeira_aula: async (curso_id) => { 303 const aulas = await vAPI_cursos.get_links_aulas(curso_id); 304 if (!aulas || typeof aulas !== "object") return false; 305 306 if (aulas.presencial) return aulas.presencial; 307 308 const ids = Object.keys(aulas); 309 if (!ids.length) return false; 310 311 ids.sort((a, b) => (aulas[a]?.order ?? 0) - (aulas[b]?.order ?? 0)); 312 const primeira = aulas[ids[0]]; 313 314 // Presenciais vêm como string; online, como { link, order }. 315 return typeof primeira === "string" ? primeira : primeira?.link || false; 316 }, 317 post_avaliacao: async (user_token, curso_id, answers) => { 318 let response = false; 319 if ( 320 user_token && 321 curso_id && 322 typeof answers == "object" && 323 Object.keys(answers).length 324 ) { 325 const api_url = `${api_cursos}/curso/${curso_id}/avaliacao`; 326 const headers = new Headers({ 327 Accept: "*/*", 328 "Content-Type": "application/json", 329 Authorization: `Bearer ${user_token}`, 330 }); 331 response = await fetch(api_url, { 332 headers: headers, 333 method: "POST", 334 body: JSON.stringify({ resposta: answers }), 335 }).catch((error) => { 336 console.error(error); 337 }); 338 response = !!(response && response.ok && response.status == 200); 339 } 340 return response; 341 }, 342 get_avaliacao: async (user_token, curso_id) => { 343 let answers = false; 344 if (user_token && curso_id) { 345 const api_url = `${api_cursos}/curso/${curso_id}/avaliacao`; 346 const headers = new Headers({ 347 Accept: "*/*", 348 "Content-Type": "application/json", 349 Authorization: `Bearer ${user_token}`, 350 }); 351 response = await fetch(api_url, { 352 headers: headers, 353 cache: "no-store", 354 }).catch((error) => { 355 console.error(error); 356 }); 357 if (response && response.ok && response.status == 200) { 358 answers = await response.json(); 359 } 360 } 361 return answers; 362 }, 363 get_media_avaliacao_curso: async (curso_id) => { 364 let answers = false;
365 const wp_token = helper_api_cursos.wp_token; 366 if (curso_id) { 367 const api_url = `${api_cursos}/curso/${curso_id}/media`; 368 const headers = new Headers({ 369 Accept: "*/*", 370 "Content-Type": "application/json", 371 Authorization: `Bearer ${wp_token}`, 372 }); 373 response = await fetch(api_url, { 374 headers: headers, 375 cache: "no-store", 376 }).catch((error) => { 377 console.error(error); 378 }); 379 if (response && response.ok && response.status == 200) { 380 answers = await response.json(); 381 } 382 } 383 return answers; 384 }, 385 get_curso_by_aula_id: async (aula_id) => { 386 let curso = false; 387 if (aula_id) { 388 const api_url = `${helper_api_cursos.url}curso/get_curso_by_aula?aula_id=${aula_id}`; 389 const response = await fetch(api_url).catch((error) => { 390 console.error(error); 391 }); 392 if (response && response.ok && response.status == 200) { 393 curso = await response.json(); 394 } 395 } 396 return curso; 397 }, 398 check_user_access_curso: async (user_token, curso_id) => { 399 let enabled = false; 400 if (user_token && curso_id) { 401 const api_url = `${api_cursos}/convidados/${curso_id}/verificar`; 402 const headers = new Headers({ 403 Authorization: `Bearer ${user_token}`, 404 }); 405 const response = await fetch(api_url, { 406 headers: headers, 407 cache: "no-store", 408 }).catch((error) => { 409 console.error(error); 410 }); 411 if (response && response.ok) enabled = await response.json(); 412 } 413 return enabled; 414 }, 415 send_question_curso: async (user_token, curso_id, question) => { 416 let sended = false; 417 418 if (user_token && curso_id && question) { 419 const api_url = `${api_cursos}/duvida/${curso_id}`; 420 const headers = new Headers({ 421 Authorization: `Bearer ${user_token}`, 422 Accept: "*/*", 423 "Content-Type": "application/json", 424 }); 425 const response = await fetch(api_url, { 426 headers: headers, 427 method: "POST", 428 body: JSON.stringify({ duvida: question }), 429 }).catch((error) => { 430 console.error(error); 431 }); 432 433 sended = !!(response && response.ok && response.status == 200); 434 } 435 return sended; 436 }, 437 get_questions: async (user_token) => { 438 let questions = []; 439 if (user_token) { 440 const api_url = `${api_cursos}/duvidas/aluno`; 441 const headers = new Headers({ 442 Authorization: `Bearer ${user_token}`, 443 }); 444 const response = await fetch(api_url, { 445 headers: headers, 446 cache: "no-store", 447 }).catch((error) => { 448 console.error(error); 449 }); 450 if (response && response.ok) { 451 const data = await response.json(); 452 questions = Array.isArray(data) ? data : (data?.data ?? []); 453 } 454 } 455 return questions; 456 }, 457 send_answer_question: async (question, answer, answered_by) => { 458 const wp_token = helper_api_cursos.wp_token; 459 if (question && answered_by) { 460 const api_url = `${api_cursos}/duvida/resposta/${question}`; 461 const headers = new Headers({ 462 Authorization: `Bearer ${wp_token}`, 463 Accept: "*/*", 464 "Content-Type": "application/json", 465 }); 466 467 const response = await fetch(api_url, { 468 headers: headers, 469 body: JSON.stringify({ 470 resposta: answer, 471 respondido_por: answered_by, 472 }), 473 method: "PUT", 474 }).catch((error) => { 475 console.error(error); 476 }); 477 478 return !!(response && response.ok && 200 == response.status); 479 } 480 return false;
481 }, 482 check_search: async (category, user_token) => { 483 let answered = false; 484 if (category && user_token) { 485 const api_url = `${api_cursos}/get_checa_pesquisa/${category}`; 486 const headers = new Headers({ 487 Authorization: `Bearer ${user_token}`, 488 Accept: "*/*", 489 "Content-Type": "application/json", 490 }); 491 492 const response = await fetch(api_url, { 493 headers: headers, 494 method: "GET", 495 cache: "no-store", 496 }).catch((error) => { 497 console.error(error); 498 }); 499 500 if (response && response.ok && 200 == response.status) { 501 answered = await response.json(); 502 } 503 } 504 return answered; 505 }, 506 check_form_has_answered: async (categoria, form, curso, user_token) => { 507 let answered = false; 508 if (categoria && form && curso && user_token) { 509 const api_url = `${api_cursos}/form/answer/check/${categoria}/${form}/${curso}`; 510 const headers = new Headers({ 511 Authorization: `Bearer ${user_token}`, 512 Accept: "*/*", 513 "Content-Type": "application/json", 514 }); 515 516 const response = await fetch(api_url, { 517 headers: headers, 518 method: "GET", 519 cache: "no-store", 520 }).catch((error) => { 521 console.error(error); 522 }); 523 524 if (response && response.ok && 200 == response.status) { 525 answered = await response.json(); 526 } 527 } 528 return answered; 529 }, 530 check_is_free: async (curso) => { 531 let is_free = true; 532 if (curso) { 533 const api_url = `${helper_api_cursos.url}curso/check_curso_is_free?curso=${curso}`; 534 const response = await fetch(api_url); 535 is_free = await response.json(); 536 } 537 return is_free; 538 }, 539 get_url_payment: (user_token, curso_id, return_url) => { 540 let url = ""; 541 if (user_token && curso_id) { 542 const data = { 543 access_token: user_token, 544 curso: curso_id, 545 return_url: return_url, 546 }; 547 url = `${api_cursos}/pagamento/${btoa(JSON.stringify(data))}`; 548 } 549 return url; 550 }, 551 check_payment_curso: async (curso_id, user_token) => { 552 let status = false; 553 if (curso_id && user_token) { 554 const api_url = `${api_cursos}/curso_pago/${curso_id}`; 555 const headers = new Headers({ 556 Authorization: `Bearer ${user_token}`, 557 Accept: "*/*", 558 "Content-Type": "application/json", 559 }); 560 561 const response = await fetch(api_url, { 562 headers: headers, 563 method: "GET", 564 cache: "no-store", 565 }).catch((error) => { 566 console.error(error); 567 }); 568 569 if (response && response.ok && 200 == response.status) { 570 status = await response.json(); 571 } 572 } 573 return status; 574 }, 575 /** 576 * Mapa perfil da API -> propriedades do contato no HubSpot. 577 * Fonte única, usada tanto pelo salvamento do perfil quanto pelos eventos 578 * de curso: antes as duas listas divergiam e o salvamento deixava de enviar 579 * cpf, cep, gênero e data de nascimento. 580 * @returns {Array<Object>} - Mapa ('de' = HubSpot, 'para' = campo do perfil) 581 */ 582 get_mapa_perfil_hubspot: () => { 583 return [ 584 {'de': 'firstname', 'para': 'nome'}, 585 {'de': 'lastname', 'para': 'sobrenome'}, 586 {'de': 'state', 'para': 'estado'}, // Estado (Ex: MG) 587 {'de': 'city', 'para': 'cidade'}, // Cidade 588 {'de': 'zip', 'para': 'cep'}, 589 {'de': 'cpf', 'para': 'cpf'}, // CPF 590 {'de': 'genero', 'para': 'genero'}, // Gênero 591 {'de': 'date_of_birth', 'para': 'data_nascimento'}, // Data de Nascimento 592 {'de': 'phone', 'para': 'telefone'}, // Telefone 593 ]; 594 }, 595 /** 596 * Prepara os dados do perfil do usuário para o envio ao HubSpot. 597 * @param {Object} user_data - Dados do perfil. 598 * @param {string} email - E-mail do usuário. 599 * @returns {Object} - Payload no formato { identificador, propriedades }. 600 */ 601 prepare_user_data_hubspot: (user_data, email) => { 602 let data = {}; 603 if ( 604 email && 605 user_data && 606 typeof user_data == "object" && 607 Object.keys(user_data).length 608 ) { 609 const propriedades = { email: email };
610 vAPI_cursos.get_mapa_perfil_hubspot().forEach((campo) => { 611 const valor = user_data[campo.para]; 612 if (valor !== undefined && valor !== null && valor !== "") { 613 propriedades[campo.de] = valor; 614 } 615 }); 616 data = { 617 identificador: "Perfil Curso", 618 propriedades: propriedades, 619 // O nome do evento é opaco na timeline; a frase diz o que houve. 620 detalhes: { mensagem: "Perfil atualizado" }, 621 }; 622 } 623 return data; 624 }, 625 /** 626 * Prepara os dados do usuário para o envio ao HubSpot. 627 * @param {Object} user_data - Dados do usuário. 628 * @param {Object} curso_data - Dados do curso. 629 * @param {Object} user_perfil - Perfil do usuário. 630 * @param {string} [evento] - Evento para registro (padrão: "MatrÃcula Curso"). 631 * @param {Object} [detalhes] - Linhas extras da nota de histórico (qtd_modulos, 632 * qtd_aulas, modulo, aula, status, mensagem). 633 * Não viram propriedades do contato. 634 * @returns {Object} - Payload no formato { identificador, propriedades, detalhes }. 635 */ 636 prepare_data_registration_curso: (user_data, curso_data, user_perfil, evento, detalhes) => { 637 638 /** Popula o objeto de propriedades a partir de um mapa de chaves. 639 * @param {Array<Object>} keys - Mapa de chaves ('de' = HubSpot, 'para' = origem) 640 * @param {Object} object - Objeto de origem dos dados 641 * @param {Object} propriedades - Objeto de propriedades em construção 642 * @returns {Object} - Objeto de propriedades */ 643 function propriedades_populate (keys, object, propriedades) { 644 for (let key of keys) { 645 let valor = key.para_2 ? object[key.para]?.[key.para_2] : object[key.para]; 646 if (valor !== undefined && valor !== null && valor !== "") { 647 propriedades[key.de] = valor; 648 } 649 } 650 return propriedades; 651 } 652 653 if (!evento) { 654 evento = "MatrÃcula Curso"; 655 } 656 let propriedades = {}; 657 let data_keys = { 658 // Campo 'de': Nome da propriedade no HubSpot 659 // Campo 'para': Nome do campo nas variáveis de usuário, curso e perfil 660 'user_data': [ // Dados do Usuário 661 {'de': 'email', 'para': 'email'}, 662 {'de': 'firstname', 'para': 'displayName'} 663 ], 664 'curso_data': [ // Dados do Curso 665 // 'nome' vem do objeto localizado na página do curso; 666 // 'name' vem do objeto retornado pela API nas páginas de aula. 667 {'de': 'tema_curso', 'para': 'nome'}, 668 {'de': 'tema_curso', 'para': 'name'}, 669 {'de': 'projeto_curso', 'para': 'o_projeto', 'para_2': 'name'}, 670 // Volume do curso. Vai como propriedade (e não só como linha da 671 // nota) porque nota não é filtrável: automação que compara 672 // "aulas feitas x aulas do curso" precisa do número no contato. 673 {'de': 'qtd_modulos_curso', 'para': 'qtd_modulos'}, 674 {'de': 'aulas_do_curso', 'para': 'qtd_aulas'} 675 ], 676 // Sobrescrevem o displayName quando o perfil está preenchido 677 'user_perfil': vAPI_cursos.get_mapa_perfil_hubspot() 678 }; 679 // Os três blocos são independentes: antes o perfil e o curso só entravam 680 // se o objeto do Firebase tivesse chaves próprias, e um evento sem 681 // user_data saÃa vazio â sem e-mail, o envio era descartado em silêncio. 682 if (user_data && typeof user_data == "object" && Object.keys(user_data).length) { 683 propriedades = propriedades_populate(data_keys.user_data, user_data, propriedades); 684 } 685 if (curso_data && typeof curso_data == "object" && Object.keys(curso_data).length) { 686 propriedades = propriedades_populate(data_keys.curso_data, curso_data, propriedades); 687 } 688 if (user_perfil && typeof user_perfil == "object" && Object.keys(user_perfil).length) { 689 propriedades = propriedades_populate(data_keys.user_perfil, user_perfil, propriedades); 690 // O e-mail é a chave de deduplicação no HubSpot: sem ele o envio não 691 // acontece. O perfil da API serve de origem quando user_data não veio. 692 if (!propriedades.email && user_perfil.email) propriedades.email = user_perfil.email; 693 } 694 const payload = { identificador: evento, propriedades: propriedades }; 695 if (detalhes && typeof detalhes == "object" && Object.keys(detalhes).length) { 696 payload.detalhes = detalhes; 697 } 698 return payload; 699 }, 700 get_project_by_id: async (project_id) => { 701 let project = false;
702 if (project_id) { 703 const api_url = `${helper_api_cursos.url}curso/get_project/?project_id=${project_id}`; 704 const response = await fetch(api_url).catch((error) => { 705 console.error(error); 706 }); 707 if (response && response.ok && response.status == 200) { 708 project = await response.json(); 709 } 710 } 711 return project; 712 }, 713 /** 714 * Envia o lead para o HubSpot através do proxy do tema. 715 * O Private App Token permanece no servidor â nunca é exposto no navegador. 716 * @param {Object} user_data - Payload de prepare_data_registration_curso / prepare_user_data_hubspot. 717 * @returns {Promise<boolean>} 718 */ 719 send_user_data_hubspot: async (user_data) => { 720 let success = false; 721 if ( 722 user_data && 723 typeof user_data == "object" && 724 user_data.propriedades && 725 user_data.propriedades.email 726 ) { 727 const url = `${helper_api_cursos.url}curso/send_hubspot`; 728 const headers = new Headers({ 729 Accept: "*/*", 730 "Content-Type": "application/json", 731 }); 732 733 const response = await fetch(url, { 734 headers: headers, 735 method: "POST", 736 body: JSON.stringify(user_data), 737 }).catch((error) => { 738 console.error(error); 739 }); 740 741 if (response && response.ok) { 742 const body = await response.json().catch(() => false); 743 success = !!(body && body.success); 744 } else if (response) { 745 const body = await response.json().catch(() => false); 746 console.error( 747 `Lead não enviado ao HubSpot. HTTP - Status: ${response.status}`, 748 body && body.error ? body.error : "" 749 ); 750 } 751 } 752 return success; 753 }, 754 /** 755 * Envia um evento para o histórico do HubSpot, uma única vez por chave. 756 * 757 * A trava de duplicidade é a tabela wp_eventos_rd, que guarda 758 * (curso, evento, email). Eventos de conteúdo repetem o mesmo rótulo em 759 * aulas/módulos diferentes, então a trava usa `chave` â ex.: 760 * "Aula Iniciada #123" â enquanto o HubSpot recebe o rótulo limpo em `evento`. 761 * 762 * Sem curso (ex.: o ping de login) não há como usar a tabela, que é chaveada 763 * por curso â nesses casos a repetição precisa ser barrada pelo chamador. 764 * 765 * @param {Object} opcoes 766 * @param {string} opcoes.access_token - Token do Firebase. 767 * @param {Object} opcoes.user - Usuário logado (window.user / app.current_user). 768 * @param {Object} [opcoes.curso] - Dados do curso. 769 * @param {string} opcoes.evento - Rótulo do evento no HubSpot. 770 * @param {Object} [opcoes.detalhes] - Linhas extras da nota. 771 * @param {Object} [opcoes.propriedades] - Propriedades extras do contato 772 * (progresso_do_curso, aula_atual_do_curso...). 773 * @param {string} [opcoes.chave] - Chave de deduplicação (padrão: o próprio evento). 774 * @param {Object} [opcoes.perfil] - Perfil já carregado; evita uma requisição. 775 * @returns {Promise<boolean>} 776 */ 777 send_evento_hubspot: async (opcoes) => { 778 if (!opcoes) return false; 779 const { access_token, user, curso, evento, detalhes } = opcoes; 780 const chave = opcoes.chave ? opcoes.chave : evento; 781 782 if (!access_token || !evento) return false; 783 784 let perfil = opcoes.perfil; 785 if (!perfil || !perfil.email) { 786 perfil = await vAPI_cursos.get_perfil(access_token); 787 } 788 if (!perfil || !perfil.email) { 789 console.warn(`Evento (${chave}) não enviado: perfil sem e-mail.`); 790 return false; 791 } 792 793 const tem_curso = !!(curso && curso.id); 794 795 if (tem_curso) { 796 const ja_enviado = await vAPI_cursos.get_usuario_evento_hubspot(curso.id, chave, perfil.email); 797 if (ja_enviado) return false;
798 } 799 800 const data = vAPI_cursos.prepare_data_registration_curso(user, curso, perfil, evento, detalhes); 801 802 if (opcoes.propriedades && typeof opcoes.propriedades == "object") { 803 for (const chave_extra in opcoes.propriedades) { 804 const valor = opcoes.propriedades[chave_extra]; 805 if (valor !== undefined && valor !== null && valor !== "") { 806 data.propriedades[chave_extra] = valor; 807 } 808 } 809 } 810 811 const enviado = await vAPI_cursos.send_user_data_hubspot(data); 812 813 // Só marca como enviado depois da confirmação. Marcar antes tornava o 814 // evento definitivamente perdido quando o HubSpot devolvia 429 ou 500: 815 // a trava já estava gravada e nenhuma visita futura tentaria de novo. 816 if (enviado && tem_curso) { 817 await vAPI_cursos.post_usuario_evento_hubspot(curso.id, chave, perfil.email); 818 } 819 820 if (!enviado) { 821 console.warn(`Evento não enviado ao HubSpot: ${evento}`); 822 } 823 return enviado; 824 }, 825 get_current_date: async () => { 826 const api_url = `${helper_api_cursos.url}curso/current_date`; 827 const response = await fetch(api_url); 828 const data = await response.json(); 829 return data; 830 }, 831 send_mail_registration: async ( user_token, curso_id ) => { 832 let success = false; 833 const url = `${helper_api_cursos.url}curso/send_email`; 834 const headers = new Headers({ 835 'User-Token': user_token, 836 Accept: "*/*", 837 "Content-Type": "application/json", 838 }); 839 840 fetch( url, { 841 headers: headers, 842 method: "POST", 843 body: JSON.stringify({ 844 template: 'usuario_inscrito_curso_presencial', 845 curso_id: curso_id, 846 }), 847 } ).then( reponse => { 848 if ( reponse.ok ) { 849 success = true; 850 } else { 851 throw new Error(`E-mail de inscrição não enviado. HTTP - Status: ${reponse.status}`); 852 } 853 }).catch( ( erro ) => { 854 console.error( erro ); 855 }); 856 return success; 857 }, 858 get_usuario_evento_hubspot: async (curso_id, evento, email, debug) => { 859 let check = false; 860 if (curso_id && evento && email) { 861 const api_url = `${helper_api_cursos.url}curso/get_usuario_evento_hubspot/?curso_id=${curso_id}&evento=${encodeURIComponent(evento)}&email=${encodeURIComponent(email)}`; 862 const response = await fetch(api_url).catch((error) => { 863 console.error(error); 864 }); 865 if (response && response.ok && response.status == 200) { 866 check = await response.json(); 867 } 868 } 869 return check; 870 }, 871 post_usuario_evento_hubspot: async ( curso_id, evento, email, debug ) => { 872 let check = false; 873 874 if ( curso_id && evento && email ) { 875 const url = `${helper_api_cursos.url}curso/post_usuario_evento_hubspot`; 876 let cabecalho_obj = { 877 Accept: "*/*", 878 "Content-Type": "application/json", 879 }; 880 if ( debug && debug.betas ) { 881 cabecalho_obj.Authorization = 'Basic ' + btoa('confere:conferido'); 882 } 883 const cabecalho = new Headers(cabecalho_obj); 884 885 const response = await fetch( url, { 886 headers: cabecalho, 887 method: "POST", 888 body: JSON.stringify({ curso_id: curso_id, evento: evento, email: email }), 889 } ).catch( ( erro ) => { 890 console.error( erro ); 891 }); 892 893 if ( response && response.ok ) { 894 check = true; 895 } else if ( response ) { 896 console.error( `Evento não armazenado. HTTP - Status: ${response.status}` ); 897 } 898 } 899 return check; 900 }, 901 validar_perfil: async (perfil) => { 902 let validado = false; 903 // Valida apenas presença dos campos obrigatórios (ignora tipo/enum para evitar 904 // erros de strict mode do AJV com valores salvos em versões anteriores do formulário) 905 const ajv = new ajv7(); 906 const validate = ajv.compile( { type: 'object', required: perfil_schema.required } ); 907 const teste_validar = validate( perfil ); 908 if ( teste_validar ) { 909 validado = teste_validar; 910 } 911 return validado; 912 913 } 914};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.