/* Checkout bridge only: approved commercial functions in web/index.html stay intact. */ (() => { const status = document.getElementById('checkoutStatus'); const actions = document.getElementById('checkoutActions'); let busy = false; let draftId = localStorage.getItem('dtf-quote'); let requestKey = localStorage.getItem('dtf-request-key'); let requestBody = localStorage.getItem('dtf-request-body'); let quotedCart = localStorage.getItem('dtf-quote-cart'); let refreshVersion = 0; function cartSnapshot() { const items=[...pedido,...(itemAtual?[itemAtual]:[])]; return JSON.stringify({customer:cliente,delivery:entrega,items:items.map(item=>({ mode:item.modo,metres:item.metros,grade:item.nota,production:item.production, quality:item.qualityStatus, acknowledged:item.qualityAcknowledged, files:(item.localFiles||[]).map(file=>({name:file.name,size:file.size,lastModified:file.lastModified})) }))}); } function clearDraft() { draftId=null;quotedCart=null;requestKey=null;requestBody=null; for(const key of ['dtf-quote','dtf-quote-cart','dtf-request-key','dtf-request-body']) localStorage.removeItem(key); actions.replaceChildren(); } const api = async (path, body) => { const response = await fetch('/api'+path, { credentials: 'same-origin', headers: {'Content-Type':'application/json'}, ...(body === undefined ? {} : {method:'POST', body:JSON.stringify(body)}) }); const text = await response.text(); let data; try { data = text ? JSON.parse(text) : {}; } catch (_) { throw new Error(response.ok ? 'Resposta inválida do serviço.' : 'O serviço está indisponível. Tente novamente em instantes.'); } if (!response.ok) { const error=new Error(typeof data.detail === 'string' ? data.detail : 'Confira os dados do pedido ('+response.status+').');error.status=response.status;throw error; } return data; }; const ready = api('/session'); ready.then(session=>{ window.dtfUploadMaxBytes=session.max_upload_bytes; const limit=document.getElementById('zLimite'); if(limit)limit.textContent='Até '+(session.max_upload_bytes/1048576).toFixed(0)+ ' MB por arquivo enquanto a verificação de segurança para arquivos grandes é preparada.'; }).catch(()=>{}); window.dtfSessionReady=ready; window.dtfApi=api; ready.catch(error => { status.textContent = error.message; }); function message(text) { status.textContent = text; } function button(label, handler) { const el = document.createElement('button'); el.textContent = label; el.style.cssText = 'margin:8px 8px 0 0;padding:8px 14px;cursor:pointer'; el.onclick = handler; actions.append(el); return el; } async function upload(file) { const session=await ready; return window.dtfUpload(file,{api,progress:message,scope:session.cart_scope}); } window.dtfFreight = async () => { const cep = entrega.cep; try { const result = await api('/freight',{service:'mock-standard',postal_code:cep}); if (entrega.cep !== cep || entrega.tipo !== 'frete') return; entrega.valor = result.total_cents/100; entrega.cotado = true; $('cepMsg').textContent = 'Frete estimado: '+rs(entrega.valor)+'.'; pintaEntrega(); } catch(error) { $('cepMsg').textContent = error.message; } }; window.dtfCheckout = async () => { if (busy) return; if (draftId) { await refresh(); status.scrollIntoView({behavior:'smooth',block:'nearest'}); return; } if (!clienteOk() || !entrega.cotado) return; if (!enderecoOk()) return message('Preencha o endereço de entrega.'); if (!cartPodeEnviar()) return message('Revise a qualidade e confirme a ressalva de cada item antes de enviar o pedido.'); const cart = [...pedido,...(itemAtual?[itemAtual]:[])]; if (!cart.length) return message('Adicione um item ao pedido.'); const initialCart=cartSnapshot(); busy = true; $('bPagar').disabled = true; try { await ready; if((await api('/session')).cart_scope !== (await ready).cart_scope) throw new Error('Sua conta ou sessão mudou. Recarregue a página antes de enviar o carrinho.'); const items=[]; for (const item of cart) { if (!item.localFiles?.length) throw new Error('Selecione novamente os arquivos deste item.'); const uploads=[]; for (const file of item.localFiles) uploads.push(await upload(file)); if (item.production?.version!==2 || item.production.sources?.length!==uploads.length) throw new Error('A montagem deste item precisa ser refeita antes da cotação.'); items.push({mode:item.modo, metres:String(item.metros), grade:item.nota, uploads, production:{...item.production,sources:item.production.sources.map((source,index)=>({ upload_id:uploads[index],...source}))}, quality_status:item.qualityStatus,quality_acknowledged:item.qualityAcknowledged}); } const content = {customer:{...cliente},items,freight:{service:entrega.tipo==='retira'?'pickup':'mock-standard',postal_code:entrega.tipo==='retira'?'':entrega.cep}}; const destination = destinoApi(); if (destination) content.destination = destination; if (cartSnapshot()!==initialCart) throw new Error('O carrinho mudou durante o envio. Confira os itens e envie de novo.'); const serialized = JSON.stringify(content); if (!requestKey || serialized !== requestBody) { requestKey = crypto.randomUUID(); requestBody = serialized; localStorage.setItem('dtf-request-key',requestKey); localStorage.setItem('dtf-request-body',requestBody); } const quote = await api('/quotes',{request_key:requestKey,...content}); quotedCart=initialCart;localStorage.setItem('dtf-quote-cart',quotedCart); draftId=quote.id; localStorage.setItem('dtf-quote',draftId); await refresh(); status.scrollIntoView({behavior:'smooth',block:'nearest'}); } catch(error) { message(error.message); } finally { busy=false; pintaEntrega(); } }; async function refresh() { if (!draftId) return; const version=++refreshVersion, shownId=draftId; try { await ready; const quote=await api('/quotes/'+shownId); if(version!==refreshVersion || draftId!==shownId) return; actions.replaceChildren(); if (quote.status!=='paid' && (!quotedCart || quotedCart!==cartSnapshot())) { message('O carrinho mudou ou não está disponível neste navegador. A cotação anterior continua separada; envie o carrinho atual para uma nova revisão.'); button('Enviar carrinho atual',()=>{clearDraft();window.dtfCheckout();}); return; } if (quote.status==='pending_review') { message('Arquivos enviados. No Kanban, confira metragem e nota e aprove a cotação '+draftId.slice(0,8)+'.'); } else if (quote.status==='approved') { message('Total validado no servidor: '+rs(quote.approved.total_cents/100)+' · inclui frete. Cotação válida por 24 horas.'); if ((await ready).payment_provider === 'mercadopago') { button('Pagar com PIX',async event=>{ if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; } event.target.disabled=true; try { showPix(await api('/payments/intent',{quote_id:draftId,method:{type:'pix'}})); } catch(error) { message(error.message); event.target.disabled=false; } }); if ((await ready).payment_public_key) { button('Pagar com cartão',async event=>{ if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; } event.target.disabled=true; try { await showCard(quote.approved.total_cents); } catch(error) { message(error.message); event.target.disabled=false; } }); } return; } if ((await ready).environment !== 'local') { message('Cotação revisada. O pagamento online ainda não está disponível.'); return; } button('Criar pedido de teste',async event=>{ if (!quotedCart || quotedCart!==cartSnapshot()) { await refresh(); return; } event.target.disabled=true; try { const order=await api('/orders/dev-paid',{quote_id:draftId}); pedido=[]; itemAtual=null; limpaPaineis(); await window.dtfClearCart?.(); message('Pedido #'+order.number+' criado e disponível no Kanban.'); await refresh(); } catch(error) { message(error.message); event.target.disabled=false; } }); } else if (quote.status==='paid') { message('Pedido #'+quote.order.number+' · etapa: '+quote.order.state+'.'); button('Novo pedido',()=>{clearDraft();location.reload();}); } else { message('Cotação expirada. Envie o carrinho para uma nova revisão.'); button('Nova cotação',clearDraft); } } catch(error) { if(version!==refreshVersion || draftId!==shownId) return; message(error.message); actions.replaceChildren(); button('Limpar referência e tentar de novo',clearDraft); } } // PIX: the provider's QR code and copy-and-paste code. The order is created // by the provider's notification, not by this page, so the page only waits. let pixTimer=null; function showPix(intent) { actions.replaceChildren(); if (!intent.pix_qr_code) { message('Não foi possível gerar o PIX. Tente de novo em instantes.'); return; } message('Pague o PIX de '+rs(intent.total_cents/100)+'. O pedido entra na produção assim que o pagamento for confirmado.'); if (intent.pix_qr_code_base64) { const img=document.createElement('img'); img.src='data:image/png;base64,'+intent.pix_qr_code_base64; img.alt='QR code do PIX';img.width=220;img.height=220;img.style.display='block'; actions.append(img); } const code=document.createElement('input'); code.readOnly=true;code.value=intent.pix_qr_code;code.style.cssText='width:100%;margin-top:8px;padding:8px'; actions.append(code); button('Copiar código PIX',async()=>{ try{ await navigator.clipboard.writeText(intent.pix_qr_code); message('Código copiado.'); }catch(_){ code.select(); } }); clearInterval(pixTimer); pixTimer=setInterval(async()=>{ try { const quote=await api('/quotes/'+draftId); if (quote.status==='paid') { clearInterval(pixTimer); pedido=[]; itemAtual=null; limpaPaineis(); await window.dtfClearCart?.(); await refresh(); } } catch(_) {} },5000); } // Card: Mercado Pago's own form (Card Payment Brick). The card is typed into // Mercado Pago's secure fields and becomes a one-time token; the number never // reaches this page's code or our server. As with PIX, the order is created // by the provider's notification, so the page only waits for it. let sdkLoading=null; function loadMercadoPago() { if (window.MercadoPago) return Promise.resolve(); sdkLoading = sdkLoading || new Promise((resolve,reject)=>{ const script=document.createElement('script'); script.src='https://sdk.mercadopago.com/js/v2'; script.onload=resolve; script.onerror=()=>{sdkLoading=null;reject(new Error('Não foi possível carregar o formulário do Mercado Pago.'));}; document.head.append(script); }); return sdkLoading; } let cardBrick=null; async function showCard(totalCents) { await loadMercadoPago(); const session=await ready; actions.replaceChildren(); const holder=document.createElement('div'); holder.id='cardPaymentBrick'; holder.style.cssText='max-width:520px;margin-top:8px'; actions.append(holder); message('Pagamento com cartão: '+rs(totalCents/100)+'.'); if (cardBrick) { try { await cardBrick.unmount(); } catch(_) {} } const mp=new window.MercadoPago(session.payment_public_key,{locale:'pt-BR'}); cardBrick=await mp.bricks().create('cardPayment','cardPaymentBrick',{ initialization:{amount:totalCents/100, payer:{email:cliente.mail}}, customization:{paymentMethods:{maxInstallments:12}}, callbacks:{ onReady:()=>{}, onError:error=>{ console.error(error); message('Erro no formulário do cartão. Confira os dados e tente de novo.'); }, onSubmit:async data=>{ const result=await api('/payments/intent',{quote_id:draftId,method:{ type:'card', token:data.token, payment_method_id:data.payment_method_id, installments:Number(data.installments)||1, issuer_id:data.issuer_id==null?null:String(data.issuer_id)}}); if (result.status==='approved' || result.status==='pending') { message(result.status==='approved' ? 'Pagamento aprovado. Seu pedido entra na produção em instantes.' : 'Pagamento em análise pelo Mercado Pago. Avisamos assim que for confirmado.'); waitForOrder(); } else { message('Pagamento recusado pelo Mercado Pago ('+(result.status_detail||result.status)+'). '+ 'Confira os dados ou use outro cartão.'); throw new Error('rejected'); } } } }); } function waitForOrder() { clearInterval(pixTimer); pixTimer=setInterval(async()=>{ try { const quote=await api('/quotes/'+draftId); if (quote.status==='paid') { clearInterval(pixTimer); pedido=[]; itemAtual=null; limpaPaineis(); await window.dtfClearCart?.(); await refresh(); } } catch(_) {} },5000); } const quoteFromPortal=new URLSearchParams(location.search).get('quote'); if(quoteFromPortal && /^[0-9a-f-]{36}$/.test(quoteFromPortal)){ if(draftId!==quoteFromPortal){quotedCart=null;localStorage.removeItem('dtf-quote-cart');} draftId=quoteFromPortal;localStorage.setItem('dtf-quote',draftId); } window.addEventListener('dtf-cart-changed',()=>{if(draftId) refresh();}); refresh(); })();