Add Brandfetch merchant logos to dashboard
- web/logos.js: high-precision alias map (description regex -> brand domain) feeding the Brandfetch logo CDN; only attaches a logo when a merchant confidently maps to a known brand, else a coloured-initial fallback. - Logos rendered in: top-merchants list, category breakdown rows, ledger. - scripts/resolve-logos.mjs: offline resolver (alias + Brandfetch search) that measures coverage and writes merchant-logos.json. Current: 33 brands cover ~35% of expense txns (Uber, MercadoPago, Apple, Rappi, Santa Isabel, Copec, etc.); the long tail is local businesses with no brand -> fallback. - Client ID is public (CDN); no secret key in the repo.
This commit is contained in:
parent
e1eed9e5b4
commit
e0f2252124
|
|
@ -0,0 +1,127 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
const CLIENT_ID='1idZRzFTMDBLO5z-dbd';
|
||||||
|
const LEDGERS=['web/ledger.json','web/ledger-backfill.json'];
|
||||||
|
|
||||||
|
// High-frequency / important brands: regex on the RAW upper description -> canonical domain.
|
||||||
|
// These are resolved directly (logo via Brandfetch CDN by domain) with high precision.
|
||||||
|
const ALIAS=[
|
||||||
|
[/UBER\s*EATS|UBR.*EATS|PS UBER EATS/, 'Uber Eats','ubereats.com'],
|
||||||
|
[/\bUBER\b|\bUBR\b/, 'Uber','uber.com'],
|
||||||
|
[/RAPPI/, 'Rappi','rappi.com'],
|
||||||
|
[/MERCADO ?PAGO|MERPAGO|\bMP\b/, 'Mercado Pago','mercadopago.cl'],
|
||||||
|
[/APPLE/, 'Apple','apple.com'],
|
||||||
|
[/NETFLIX/, 'Netflix','netflix.com'],
|
||||||
|
[/SPOTIFY/, 'Spotify','spotify.com'],
|
||||||
|
[/\bTEMU\b/, 'Temu','temu.com'],
|
||||||
|
[/ALIEXPRESS/, 'AliExpress','aliexpress.com'],
|
||||||
|
[/\bAMAZON\b/, 'Amazon','amazon.com'],
|
||||||
|
[/\bSHEIN\b/, 'Shein','shein.com'],
|
||||||
|
[/CABIFY/, 'Cabify','cabify.com'],
|
||||||
|
[/\bDIDI\b/, 'DiDi','didiglobal.com'],
|
||||||
|
[/PAYPAL/, 'PayPal','paypal.com'],
|
||||||
|
[/PAYSEND/, 'Paysend','paysend.com'],
|
||||||
|
[/\bMC ?DONALD/, "McDonald's",'mcdonalds.com'],
|
||||||
|
[/\bKFC\b/, 'KFC','kfc.com'],
|
||||||
|
[/STA ISABEL|SANTA ISABEL/, 'Santa Isabel','santaisabel.cl'],
|
||||||
|
[/\bJUMBO\b/, 'Jumbo','jumbo.cl'],
|
||||||
|
[/HIP ?LIDER|\bLIDER\b/, 'Lider','lider.cl'],
|
||||||
|
[/TOTTUS/, 'Tottus','tottus.cl'],
|
||||||
|
[/UNIMARC/, 'Unimarc','unimarc.cl'],
|
||||||
|
[/\bCOPEC\b/, 'Copec','copec.cl'],
|
||||||
|
[/\bSHELL\b/, 'Shell','shell.com'],
|
||||||
|
[/FALABELLA/, 'Falabella','falabella.com'],
|
||||||
|
[/RIPLEY/, 'Ripley','ripley.cl'],
|
||||||
|
[/SODIMAC/, 'Sodimac','sodimac.cl'],
|
||||||
|
[/\bPARIS\b/, 'Paris','paris.cl'],
|
||||||
|
[/\bENTEL\b/, 'Entel','entel.cl'],
|
||||||
|
[/\bWOM\b/, 'WOM','wom.cl'],
|
||||||
|
[/MOVISTAR/, 'Movistar','movistar.cl'],
|
||||||
|
[/\bCLARO\b/, 'Claro','claro.cl'],
|
||||||
|
[/TURBUS|TUR BUS/, 'Turbus','turbus.cl'],
|
||||||
|
[/CRUZ VERDE/, 'Cruz Verde','cruzverde.cl'],
|
||||||
|
[/SALCOBRAND/, 'Salcobrand','salcobrand.cl'],
|
||||||
|
[/FARMACIA.*AHUMADA|AHUMADA/, 'Farmacias Ahumada','farmaciasahumada.cl'],
|
||||||
|
[/INTEGRAMEDICA/, 'IntegraMédica','integramedica.cl'],
|
||||||
|
[/\bGOOGLE\b/, 'Google','google.com'],
|
||||||
|
[/MICROSOFT/, 'Microsoft','microsoft.com'],
|
||||||
|
[/\bOPENAI\b/, 'OpenAI','openai.com'],
|
||||||
|
[/ANTHROPIC/, 'Anthropic','anthropic.com'],
|
||||||
|
[/HETZNER/, 'Hetzner','hetzner.com'],
|
||||||
|
[/SPOTMINDERS/, 'Spotminders','spotminders.com'],
|
||||||
|
[/ADIDAS/, 'adidas','adidas.com'],
|
||||||
|
[/NEW BALANCE/, 'New Balance','newbalance.com'],
|
||||||
|
[/ENEL/, 'Enel','enel.cl'],
|
||||||
|
[/SERVIPAG/, 'Servipag','servipag.com'],
|
||||||
|
[/SENCILLITO/, 'Sencillito','sencillito.com'],
|
||||||
|
];
|
||||||
|
|
||||||
|
function aliasOf(descRaw){
|
||||||
|
const s=(descRaw||'').toUpperCase();
|
||||||
|
for(const [re,name,domain] of ALIAS) if(re.test(s)) return {name,domain,via:'alias'};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function brandTerm(descRaw){
|
||||||
|
let s=(descRaw||'').toUpperCase();
|
||||||
|
if(/INTERES|IMPUEST|COMISION|COM\.|MANTENCION|PASAJE QR|RENDIMIENTO|COBRANZA|\bMORA\b|DL ?3475|CARGO INTERES|GASTO DE|TRASPASO|\bABONO\b|\bGIRO\b|RETIRO|TEF |TRANSF|SEGURO|DIVISAS|PREPAGO EN CUOTAS|PAGO (AUTOMATICO|APP|TARJETA)|O\.GERENCIA/.test(s)) return null;
|
||||||
|
s=s.replace(/^(PS|PAGO|PAGO:|PREPAGO|COMPRA)\s+/i,'');
|
||||||
|
s=s.replace(/^(PAYU|MP|CV ?\d*|DL|PAYSCAN|MERPAGO|MERCADOPAGO|SUMUP|TUU|MACH|WEBPAY|ONECLICK|KUSHKI|SERVIPAG\.COM)\s*[\*—\-·:]*\s*/i,'');
|
||||||
|
s=s.replace(/\b(COMPRA POR INTERNET|COMPRA NACIONAL|COMPRA INTERNAC\w*|COMPRA( \d+)? CUOTAS( PRECIO CONTADO)?|PRECIO CONTADO|RECARGA ONLINE|WEB ?\d*|ONLINE)\b/gi,' ');
|
||||||
|
s=s.replace(/·.*/,' ').replace(/\.(COM|CO|CL)\b.*/i,' ').replace(/HELP\..*/i,' ');
|
||||||
|
s=s.replace(/\b(PENDING|PEND|SANTIAGO|CL|LAS CONDES|PROVIDENCIA|VITACURA|CURICO)\b/g,' ');
|
||||||
|
s=s.replace(/[\*—·|\/]/g,' ').replace(/\s{2,}/g,' ').trim();
|
||||||
|
if(s.length<3) return null;
|
||||||
|
if(/^(COMPRA|PAGO|TARJETA|NACIONAL|INTERNET|VENTA|DIGITAL|VIRTUAL|PUN|WEB)$/.test(s)) return null;
|
||||||
|
return s.slice(0,40);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasHits=new Map(); const terms=new Map();
|
||||||
|
let totalTx=0, aliasTx=0;
|
||||||
|
for(const lf of LEDGERS){ if(!fs.existsSync(lf))continue;
|
||||||
|
const d=JSON.parse(fs.readFileSync(lf));
|
||||||
|
for(const s of d.statements) for(const t of s.transactions){
|
||||||
|
if(!['expense','fee'].includes(t.flow_type))continue;
|
||||||
|
totalTx++;
|
||||||
|
const a=aliasOf(t.description);
|
||||||
|
if(a){ aliasHits.set(a.domain,a); aliasTx++; continue; }
|
||||||
|
const term=brandTerm(t.description); if(!term)continue;
|
||||||
|
terms.set(term,(terms.get(term)||0)+1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||||
|
async function search(q){ try{ const r=await fetch('https://api.brandfetch.io/v2/search/'+encodeURIComponent(q)+'?c='+CLIENT_ID); if(!r.ok)return null; const j=await r.json(); return Array.isArray(j)?j:null;}catch{return null;} }
|
||||||
|
function norm(x){return (x||'').toUpperCase().replace(/[^A-Z0-9]/g,'');}
|
||||||
|
function pick(term,results){
|
||||||
|
if(!results)return null;
|
||||||
|
const words=term.toUpperCase().split(/\s+/).filter(w=>w.length>2);
|
||||||
|
for(const b of results){
|
||||||
|
if(!b.verified||!b.icon||b.qualityScore<0.6)continue;
|
||||||
|
const N=norm(b.name);
|
||||||
|
const firstWordMatch = words.length && N.includes(norm(words[0])) && norm(words[0]).length>2;
|
||||||
|
const whole = norm(term);
|
||||||
|
if(firstWordMatch || N.includes(whole) || whole.includes(N)) return {name:b.name,domain:b.domain,via:'search'};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const out={};
|
||||||
|
for(const a of aliasHits.values()) out[a.name.toUpperCase()]={name:a.name,domain:a.domain,via:'alias'};
|
||||||
|
let shit=0,smiss=0; const missed=[];
|
||||||
|
for(const [term,cnt] of [...terms.entries()].sort((a,b)=>b[1]-a[1])){
|
||||||
|
const m=pick(term, await search(term));
|
||||||
|
if(m){ out[term]=m; shit++; } else { smiss++; if(missed.length<20) missed.push(term+' ('+cnt+')'); }
|
||||||
|
await sleep(110);
|
||||||
|
}
|
||||||
|
// build coverage: how many TRANSACTIONS get a logo
|
||||||
|
function logoForTx(desc){
|
||||||
|
const a=aliasOf(desc); if(a&&out[a.name.toUpperCase()])return true;
|
||||||
|
const term=brandTerm(desc); if(term&&out[term])return true; return false;
|
||||||
|
}
|
||||||
|
let covered=0;
|
||||||
|
for(const lf of LEDGERS){ if(!fs.existsSync(lf))continue; const d=JSON.parse(fs.readFileSync(lf));
|
||||||
|
for(const s of d.statements) for(const t of s.transactions){ if(!['expense','fee'].includes(t.flow_type))continue; if(logoForTx(t.description))covered++; } }
|
||||||
|
fs.writeFileSync('web/merchant-logos.json',JSON.stringify(out,null,1));
|
||||||
|
console.log('alias brands:',aliasHits.size,'| search resolved:',shit,'| search missed:',smiss);
|
||||||
|
console.log('TOTAL logo entries:',Object.keys(out).length);
|
||||||
|
console.log('TXN COVERAGE: '+covered+' / '+totalTx+' expense+fee txns ('+Math.round(covered/totalTx*100)+'%)');
|
||||||
|
console.log('\n--- still missed (top) ---'); missed.forEach(m=>console.log(' '+m));
|
||||||
|
|
@ -213,3 +213,10 @@
|
||||||
.bk-row-pct { font-size: 11px; color: var(--ink-dim); font-family: var(--font-num); text-align: right; }
|
.bk-row-pct { font-size: 11px; color: var(--ink-dim); font-family: var(--font-num); text-align: right; }
|
||||||
.bk-row-amt { font-size: 12px; color: var(--ink); font-family: var(--font-num); text-align: right; }
|
.bk-row-amt { font-size: 12px; color: var(--ink); font-family: var(--font-num); text-align: right; }
|
||||||
.bk-more { font-size: 11px; color: var(--ink-dim); padding: 7px 0 3px 19px; }
|
.bk-more { font-size: 11px; color: var(--ink-dim); padding: 7px 0 3px 19px; }
|
||||||
|
|
||||||
|
/* ---- merchant logos (Brandfetch) ---- */
|
||||||
|
.mlogo { border-radius: 6px; object-fit: contain; background: rgba(255,255,255,0.06); vertical-align: middle; flex: none; }
|
||||||
|
.mlogo-fb { display: inline-flex; align-items: center; justify-content: center; border-radius: 6px; font-size: 11px; font-weight: 700; color: #fff; font-family: var(--font-ui); }
|
||||||
|
.mrow .m-name { display: flex; align-items: center; gap: 9px; }
|
||||||
|
.bk-row-desc.with-logo { display: flex; align-items: center; gap: 8px; }
|
||||||
|
.lx-desc .lx-logo-wrap { display: inline-flex; align-items: center; gap: 8px; }
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="logos.js"></script>
|
||||||
<script src="dashboard.js"></script>
|
<script src="dashboard.js"></script>
|
||||||
<script src="dashboard2.js"></script>
|
<script src="dashboard2.js"></script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,8 @@
|
||||||
merch.forEach(([name, d], i) => {
|
merch.forEach(([name, d], i) => {
|
||||||
const cat = window.MT.spendCategory(d.txns[0].description);
|
const cat = window.MT.spendCategory(d.txns[0].description);
|
||||||
const row = document.createElement('div'); row.className = 'mrow';
|
const row = document.createElement('div'); row.className = 'mrow';
|
||||||
row.innerHTML = `<span class="m-rank">${i + 1}</span><span class="m-name">${esc(name)}<span class="m-cat">${esc(cat)}</span></span><span class="m-cnt">${d.txns.length}×</span><span class="m-amt">${window.MT.CLPk(d.value)}</span>`;
|
const logo = window.MTlogo ? window.MTlogo.html(name, 22) : '';
|
||||||
|
row.innerHTML = `<span class="m-rank">${i + 1}</span><span class="m-name">${logo}<span>${esc(name)}<span class="m-cat">${esc(cat)}</span></span></span><span class="m-cnt">${d.txns.length}×</span><span class="m-amt">${window.MT.CLPk(d.value)}</span>`;
|
||||||
row.addEventListener('mousemove', e => window.MTtip.rows(e, name, col.spend(), d.txns));
|
row.addEventListener('mousemove', e => window.MTtip.rows(e, name, col.spend(), d.txns));
|
||||||
row.addEventListener('mouseleave', () => window.MTtip.hide());
|
row.addEventListener('mouseleave', () => window.MTtip.hide());
|
||||||
ml.appendChild(row);
|
ml.appendChild(row);
|
||||||
|
|
@ -179,8 +180,9 @@
|
||||||
section.className = 'bk-section';
|
section.className = 'bk-section';
|
||||||
const rowsHtml = descs.slice(0, 30).map(([desc, d]) => {
|
const rowsHtml = descs.slice(0, 30).map(([desc, d]) => {
|
||||||
const cpct = Math.round(d.value / catData.value * 100);
|
const cpct = Math.round(d.value / catData.value * 100);
|
||||||
|
const blogo = window.MTlogo ? window.MTlogo.html(desc, 18) : "";
|
||||||
return `<div class="bk-row">
|
return `<div class="bk-row">
|
||||||
<span class="bk-row-desc">${esc(desc)}</span>
|
<span class="bk-row-desc with-logo">${blogo}<span>${esc(desc)}</span></span>
|
||||||
<span class="bk-row-cnt">${d.txns.length}×</span>
|
<span class="bk-row-cnt">${d.txns.length}×</span>
|
||||||
<span class="bk-row-pct">${cpct}%</span>
|
<span class="bk-row-pct">${cpct}%</span>
|
||||||
<span class="bk-row-amt">${MT.CLPk(d.value)}</span>
|
<span class="bk-row-amt">${MT.CLPk(d.value)}</span>
|
||||||
|
|
|
||||||
|
|
@ -285,7 +285,7 @@
|
||||||
const shown = rows.slice(0, st.limit);
|
const shown = rows.slice(0, st.limit);
|
||||||
$('#lx-rows').innerHTML = shown.map(t => `
|
$('#lx-rows').innerHTML = shown.map(t => `
|
||||||
<tr><td class="lx-date">${t.date}</td>
|
<tr><td class="lx-date">${t.date}</td>
|
||||||
<td class="lx-desc">${esc(t.description || t.counterparty || '—')}</td>
|
<td class="lx-desc"><span class="lx-logo-wrap">${window.MTlogo ? window.MTlogo.html(t.description || t.counterparty || '', 18) : ''}<span>${esc(t.description || t.counterparty || '—')}</span></span></td>
|
||||||
<td style="white-space:nowrap;color:var(--ink-mute)">${esc(t.bank)} <span class="lx-pdf">${MT.acctShort(t.doc_type)}${t.last4 ? ' ··' + t.last4 : ''}</span></td>
|
<td style="white-space:nowrap;color:var(--ink-mute)">${esc(t.bank)} <span class="lx-pdf">${MT.acctShort(t.doc_type)}${t.last4 ? ' ··' + t.last4 : ''}</span></td>
|
||||||
<td><span class="lx-flow-tag"><i style="background:${FLOW_C[t.flow_type] || col.mute()}"></i>${FLOW_LABEL[t.flow_type] || t.flow_type}</span></td>
|
<td><span class="lx-flow-tag"><i style="background:${FLOW_C[t.flow_type] || col.mute()}"></i>${FLOW_LABEL[t.flow_type] || t.flow_type}</span></td>
|
||||||
<td class="r lx-amt ${t.direction === 'credit' ? 'cr' : 'db'}">${t.direction === 'credit' ? '+' : '−'}${CLP(t.amount)}</td></tr>`).join('');
|
<td class="r lx-amt ${t.direction === 'credit' ? 'cr' : 'db'}">${t.direction === 'credit' ? '+' : '−'}${CLP(t.amount)}</td></tr>`).join('');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
/* money-trace · merchant logos via Brandfetch CDN.
|
||||||
|
High-precision alias map (description regex -> brand domain). We only show a
|
||||||
|
logo when a description confidently maps to a known brand; everything else
|
||||||
|
falls back to a coloured initial. No guessing of arbitrary websites. */
|
||||||
|
(function(){
|
||||||
|
'use strict';
|
||||||
|
const CLIENT_ID = '1idZRzFTMDBLO5z-dbd';
|
||||||
|
// [regex on UPPERCASED description, brand name, domain]
|
||||||
|
const ALIAS = [
|
||||||
|
[/UBER\s*EATS|UBR.*EATS|PS UBER EATS/, 'Uber Eats','ubereats.com'],
|
||||||
|
[/\bUBER\b|\bUBR\b/, 'Uber','uber.com'],
|
||||||
|
[/RAPPI/, 'Rappi','rappi.com'],
|
||||||
|
[/MERCADO ?PAGO|MERPAGO|\bMP\b/, 'Mercado Pago','mercadopago.cl'],
|
||||||
|
[/APPLE/, 'Apple','apple.com'],
|
||||||
|
[/NETFLIX/, 'Netflix','netflix.com'],
|
||||||
|
[/SPOTIFY/, 'Spotify','spotify.com'],
|
||||||
|
[/\bTEMU\b/, 'Temu','temu.com'],
|
||||||
|
[/ALIEXPRESS/, 'AliExpress','aliexpress.com'],
|
||||||
|
[/\bAMAZON\b/, 'Amazon','amazon.com'],
|
||||||
|
[/\bSHEIN\b/, 'Shein','shein.com'],
|
||||||
|
[/CABIFY/, 'Cabify','cabify.com'],
|
||||||
|
[/\bDIDI\b/, 'DiDi','didiglobal.com'],
|
||||||
|
[/PAYPAL/, 'PayPal','paypal.com'],
|
||||||
|
[/PAYSEND/, 'Paysend','paysend.com'],
|
||||||
|
[/\bMC ?DONALD/, 'McDonalds','mcdonalds.com'],
|
||||||
|
[/\bKFC\b/, 'KFC','kfc.com'],
|
||||||
|
[/STA ISABEL|SANTA ISABEL/, 'Santa Isabel','santaisabel.cl'],
|
||||||
|
[/\bJUMBO\b/, 'Jumbo','jumbo.cl'],
|
||||||
|
[/HIP ?LIDER|\bLIDER\b/, 'Lider','lider.cl'],
|
||||||
|
[/TOTTUS/, 'Tottus','tottus.cl'],
|
||||||
|
[/UNIMARC/, 'Unimarc','unimarc.cl'],
|
||||||
|
[/\bCOPEC\b/, 'Copec','copec.cl'],
|
||||||
|
[/\bSHELL\b/, 'Shell','shell.com'],
|
||||||
|
[/FALABELLA/, 'Falabella','falabella.com'],
|
||||||
|
[/RIPLEY/, 'Ripley','ripley.cl'],
|
||||||
|
[/SODIMAC/, 'Sodimac','sodimac.cl'],
|
||||||
|
[/\bPARIS\b/, 'Paris','paris.cl'],
|
||||||
|
[/\bENTEL\b/, 'Entel','entel.cl'],
|
||||||
|
[/\bWOM\b/, 'WOM','wom.cl'],
|
||||||
|
[/MOVISTAR/, 'Movistar','movistar.cl'],
|
||||||
|
[/\bCLARO\b/, 'Claro','claro.cl'],
|
||||||
|
[/TURBUS|TUR BUS/, 'Turbus','turbus.cl'],
|
||||||
|
[/CRUZ VERDE/, 'Cruz Verde','cruzverde.cl'],
|
||||||
|
[/SALCOBRAND/, 'Salcobrand','salcobrand.cl'],
|
||||||
|
[/AHUMADA/, 'Farmacias Ahumada','farmaciasahumada.cl'],
|
||||||
|
[/INTEGRAMEDICA/, 'IntegraMedica','integramedica.cl'],
|
||||||
|
[/\bGOOGLE\b/, 'Google','google.com'],
|
||||||
|
[/MICROSOFT/, 'Microsoft','microsoft.com'],
|
||||||
|
[/\bOPENAI\b/, 'OpenAI','openai.com'],
|
||||||
|
[/ANTHROPIC/, 'Anthropic','anthropic.com'],
|
||||||
|
[/HETZNER/, 'Hetzner','hetzner.com'],
|
||||||
|
[/SITEGROUND/, 'SiteGround','siteground.com'],
|
||||||
|
[/ADIDAS/, 'adidas','adidas.com'],
|
||||||
|
[/NEW BALANCE/, 'New Balance','newbalance.com'],
|
||||||
|
[/\bENEL\b/, 'Enel','enel.cl'],
|
||||||
|
[/SERVIPAG/, 'Servipag','servipag.com'],
|
||||||
|
[/SENCILLITO/, 'Sencillito','sencillito.com'],
|
||||||
|
[/\bSKY\b/, 'Sky Airline','skyairline.com'],
|
||||||
|
[/LATAM/, 'LATAM','latamairlines.com'],
|
||||||
|
];
|
||||||
|
const PALETTE = ['#5b8def','#e0729a','#6fb86f','#d9a14b','#9b7fd4','#54b3c4','#cf7d56','#7d8aa0'];
|
||||||
|
const COLORS = new Map();
|
||||||
|
function colorOf(key){
|
||||||
|
if(COLORS.has(key)) return COLORS.get(key);
|
||||||
|
let h=0; for(let i=0;i<key.length;i++) h=(h*31+key.charCodeAt(i))>>>0;
|
||||||
|
const c=PALETTE[h%PALETTE.length]; COLORS.set(key,c); return c;
|
||||||
|
}
|
||||||
|
const esc = s => String(s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
function brandOf(desc){
|
||||||
|
const s=(desc||'').toUpperCase();
|
||||||
|
for(const [re,name,domain] of ALIAS) if(re.test(s)) return {name,domain};
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function initialOf(desc){
|
||||||
|
const m=(desc||'').replace(/^(PS|PAGO:?|MERPAGO|PAYU|MP|DL|CV|COMPRA|PREPAGO)\b[\s\*:—-]*/i,'').trim();
|
||||||
|
const ch=(m||desc||'?').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
|
||||||
|
return ch||'?';
|
||||||
|
}
|
||||||
|
// returns an HTML string: a brand logo <img> with initial fallback, or just the initial chip
|
||||||
|
function html(desc, size){
|
||||||
|
size = size||22;
|
||||||
|
const b = brandOf(desc);
|
||||||
|
const key = b ? b.domain : (desc||'');
|
||||||
|
const init = b ? b.name.charAt(0).toUpperCase() : initialOf(desc);
|
||||||
|
const fb = '<span class="mlogo mlogo-fb" style="width:'+size+'px;height:'+size+'px;background:'+colorOf(key)+'">'+esc(init)+'</span>';
|
||||||
|
if(!b) return fb;
|
||||||
|
const url='https://cdn.brandfetch.io/'+b.domain+'/w/'+(size*2)+'/h/'+(size*2)+'?c='+CLIENT_ID;
|
||||||
|
return '<img class="mlogo" src="'+url+'" width="'+size+'" height="'+size+'" loading="lazy" alt="'+esc(b.name)+'" title="'+esc(b.name)+'" data-fb="'+esc(fb)+'" onerror="this.outerHTML=this.getAttribute("data-fb")">';
|
||||||
|
}
|
||||||
|
window.MTlogo = { html, brandOf };
|
||||||
|
})();
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
{
|
||||||
|
"MERCADO PAGO": {
|
||||||
|
"name": "Mercado Pago",
|
||||||
|
"domain": "mercadopago.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"KFC": {
|
||||||
|
"name": "KFC",
|
||||||
|
"domain": "kfc.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"MCDONALD'S": {
|
||||||
|
"name": "McDonald's",
|
||||||
|
"domain": "mcdonalds.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"SANTA ISABEL": {
|
||||||
|
"name": "Santa Isabel",
|
||||||
|
"domain": "santaisabel.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"ENEL": {
|
||||||
|
"name": "Enel",
|
||||||
|
"domain": "enel.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"ENTEL": {
|
||||||
|
"name": "Entel",
|
||||||
|
"domain": "entel.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"HETZNER": {
|
||||||
|
"name": "Hetzner",
|
||||||
|
"domain": "hetzner.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"RAPPI": {
|
||||||
|
"name": "Rappi",
|
||||||
|
"domain": "rappi.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"UBER": {
|
||||||
|
"name": "Uber",
|
||||||
|
"domain": "uber.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"CRUZ VERDE": {
|
||||||
|
"name": "Cruz Verde",
|
||||||
|
"domain": "cruzverde.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"SERVIPAG": {
|
||||||
|
"name": "Servipag",
|
||||||
|
"domain": "servipag.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"LIDER": {
|
||||||
|
"name": "Lider",
|
||||||
|
"domain": "lider.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"UBER EATS": {
|
||||||
|
"name": "Uber Eats",
|
||||||
|
"domain": "ubereats.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"APPLE": {
|
||||||
|
"name": "Apple",
|
||||||
|
"domain": "apple.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"ALIEXPRESS": {
|
||||||
|
"name": "AliExpress",
|
||||||
|
"domain": "aliexpress.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"ANTHROPIC": {
|
||||||
|
"name": "Anthropic",
|
||||||
|
"domain": "anthropic.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"SHEIN": {
|
||||||
|
"name": "Shein",
|
||||||
|
"domain": "shein.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"COPEC": {
|
||||||
|
"name": "Copec",
|
||||||
|
"domain": "copec.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"TURBUS": {
|
||||||
|
"name": "Turbus",
|
||||||
|
"domain": "turbus.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"WOM": {
|
||||||
|
"name": "WOM",
|
||||||
|
"domain": "wom.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"GOOGLE": {
|
||||||
|
"name": "Google",
|
||||||
|
"domain": "google.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"INTEGRAMÉDICA": {
|
||||||
|
"name": "IntegraMédica",
|
||||||
|
"domain": "integramedica.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"FALABELLA": {
|
||||||
|
"name": "Falabella",
|
||||||
|
"domain": "falabella.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"JUMBO": {
|
||||||
|
"name": "Jumbo",
|
||||||
|
"domain": "jumbo.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"PAYSEND": {
|
||||||
|
"name": "Paysend",
|
||||||
|
"domain": "paysend.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"SALCOBRAND": {
|
||||||
|
"name": "Salcobrand",
|
||||||
|
"domain": "salcobrand.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"ADIDAS": {
|
||||||
|
"name": "adidas",
|
||||||
|
"domain": "adidas.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"DIDI": {
|
||||||
|
"name": "DiDi",
|
||||||
|
"domain": "didiglobal.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"UNIMARC": {
|
||||||
|
"name": "Unimarc",
|
||||||
|
"domain": "unimarc.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"CLARO": {
|
||||||
|
"name": "Claro",
|
||||||
|
"domain": "claro.cl",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"AMAZON": {
|
||||||
|
"name": "Amazon",
|
||||||
|
"domain": "amazon.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"PAYPAL": {
|
||||||
|
"name": "PayPal",
|
||||||
|
"domain": "paypal.com",
|
||||||
|
"via": "alias"
|
||||||
|
},
|
||||||
|
"TEMU": {
|
||||||
|
"name": "Temu",
|
||||||
|
"domain": "temu.com",
|
||||||
|
"via": "alias"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue