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:
Kavi 2026-06-02 14:41:09 -04:00
parent e1eed9e5b4
commit e0f2252124
7 changed files with 398 additions and 3 deletions

127
scripts/resolve-logos.mjs Normal file
View File

@ -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));

View File

@ -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; }

View File

@ -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>

View File

@ -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>

View File

@ -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('');

91
web/logos.js Normal file
View File

@ -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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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(&quot;data-fb&quot;)">';
}
window.MTlogo = { html, brandOf };
})();

167
web/merchant-logos.json Normal file
View File

@ -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"
}
}