PageSourceSearch

https://www.cursosrme.net.br/wp-content/themes/rme_cursos/assets/js/libs/api-cursos/api_cursos.js?ver=3968

js cursosrme.net.br collected 2026-09-29 13:16:52 UTC 36,972 bytes, 914 lines download raw bytes

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.