Files
soleprint/soleprint/artery/shunts/mercadopago/templates/index.html

412 lines
14 KiB
HTML
Executable File

<!DOCTYPE html>
<!--
MercadoPago shunt — config UI.
Brought onto the theme: it used to carry ~30 hardcoded hexes, its own reset,
its own button and input rules, and zero var(--…), so it was the one page in
the tree that could not follow a theme at all. Everything structural now comes
from the baked parts; what is left below is this shunt's own.
A shunt serves this on its own port and cannot fetch soleprint's /theme.css,
so the theme and the parts are baked in between markers by
common/theme/bake.py. Run `make theme bake` after changing a class.
Only `base` and `panel` are baked here — this page has no split pane, so it
carries no split.css and no split.js. That is the mechanism doing its job.
-->
<html lang="en" data-theme="soleprint">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MercadoPago API (MOCK) - Configuration</title>
<!-- theme:here -->
<!-- theme:baked-defaults — generated by common/theme/bake.py; do not edit -->
<style>
:root {
--accent: #d4a574;
--font-mono: "JetBrains Mono", "Cascadia Mono", Consolas, monospace;
--font-size-base: 13px;
--font-size-sm: 11px;
--font-ui: Inter, "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
--label-spacing: 0.04em;
--muted: #8888a0;
--panel-border: 1px solid #2e2e38;
--panel-header-height: 36px;
--panel-radius: 6px;
--space-1: 4px;
--space-2: 8px;
--space-3: 12px;
--space-4: 16px;
--space-6: 24px;
--status-error: #f06565;
--status-escalating: #f5a623;
--status-idle: #555568;
--status-live: #3ecf8e;
--status-processing: #4f9cf9;
--surface-0: #0d0d0f;
--surface-1: #16161a;
--surface-2: #1e1e24;
--surface-3: #2e2e38;
--text-dim: #555568;
--text-primary: #e8e8f0;
--text-secondary: #8888a0;
}
</style>
<!-- /theme:baked-defaults -->
<!-- theme:parts — generated by common/theme/bake.py; do not edit -->
<style>
/* part: base — common/theme/parts/base.css */
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
}
body {
background: var(--surface-0);
color: var(--text-primary);
font-family: var(--font-ui);
font-size: var(--font-size-base);
}
/* Opt in on <html>, <body> and the top element when the page is an app shell
* that should fill the viewport. Left off, the page scrolls like a document —
* which is what most ad-hoc vein pages actually want. */
.fills {
height: 100%;
width: 100%;
}
button {
font-family: var(--font-ui);
font-size: var(--font-size-base);
color: var(--text-primary);
background: var(--surface-2);
border: var(--panel-border);
border-radius: var(--panel-radius);
padding: var(--space-1) var(--space-3);
cursor: pointer;
}
button:hover:not(:disabled) {
background: var(--surface-3);
}
button:disabled {
opacity: 0.5;
cursor: default;
}
input,
select,
textarea {
font-family: var(--font-ui);
font-size: var(--font-size-base);
color: var(--text-primary);
background: var(--surface-0);
border: var(--panel-border);
border-radius: 4px;
padding: var(--space-1) var(--space-2);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--surface-3);
border-radius: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
/* part: panel — common/theme/parts/panel.css */
.panel {
position: relative;
background: var(--surface-1);
border: var(--panel-border);
border-radius: var(--panel-radius);
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
align-items: center;
gap: var(--space-2);
height: var(--panel-header-height);
padding: 0 var(--space-3);
background: var(--surface-2);
border-bottom: var(--panel-border);
flex-shrink: 0;
}
.panel-title {
font-family: var(--font-ui);
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.panel-actions {
margin-left: auto;
display: flex;
align-items: center;
gap: var(--space-2);
}
.panel-status {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
background: var(--status-idle);
}
.panel-status.idle { background: var(--status-idle); }
.panel-status.live { background: var(--status-live); }
.panel-status.processing { background: var(--status-processing); }
.panel-status.error { background: var(--status-error); }
.panel-body {
flex: 1;
overflow: auto;
padding: var(--space-2);
min-height: 0;
}
</style>
<!-- /theme:parts -->
<style>
/* This page's own, and only its own. */
body { padding: var(--space-6); }
.container { max-width: 1100px; margin: 0 auto; }
header { margin-bottom: var(--space-6); }
h1 { margin: 0; font-size: 20px; font-family: var(--font-ui); }
.subtitle { color: var(--muted); font-size: 12px; margin-top: 4px; }
.mock-badge {
display: inline-block; margin-left: var(--space-2);
padding: 2px 8px; border-radius: 3px;
background: var(--status-escalating); color: var(--surface-0);
font-size: 10px; font-weight: 600; text-transform: uppercase;
letter-spacing: var(--label-spacing, .08em); vertical-align: middle;
}
.panel { margin-bottom: var(--space-4); }
.lede { color: var(--muted); font-size: 12px; margin: 0 0 var(--space-3); }
.grid {
display: grid; gap: var(--space-2);
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
/* Selectable cards — one treatment, used by both lists. */
.card {
padding: var(--space-3); cursor: pointer; text-align: left;
background: var(--surface-2); border: var(--panel-border);
border-radius: var(--panel-radius);
}
.card:hover:not(:disabled) { border-color: var(--accent); background: var(--surface-3); }
.card.on { border-color: var(--accent); background: var(--surface-3); }
.card-name { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.card-desc { font-size: 11px; color: var(--muted); }
.verb {
font-size: 10px; font-weight: 600; padding: 1px 6px;
border-radius: 3px; border: 1px solid currentColor; margin-right: var(--space-2);
}
.verb.POST { color: var(--status-live); }
.verb.GET { color: var(--status-processing); }
.path { font-family: var(--font-mono); font-size: 12px; }
.field { margin-bottom: var(--space-3); }
.field label {
display: block; margin-bottom: 4px; font-size: 11px;
color: var(--muted); text-transform: uppercase; letter-spacing: .04em;
}
.field input, .field textarea { width: 100%; font-family: var(--font-mono); font-size: 12px; }
.field textarea { min-height: 180px; resize: vertical; }
.actions { display: flex; gap: var(--space-2); }
.primary { background: var(--accent); color: var(--surface-0); border-color: transparent; }
.primary:hover:not(:disabled) { opacity: .9; background: var(--accent); }
.url {
font-family: var(--font-mono); font-size: 12px; user-select: all;
padding: var(--space-2) var(--space-3);
background: var(--surface-0); border: var(--panel-border);
border-radius: var(--panel-radius);
}
.note { color: var(--text-dim); font-size: 11px; margin: var(--space-2) 0 0; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>MercadoPago <span class="mock-badge">mock</span></h1>
<div class="subtitle">Configure mock payment responses and behaviour</div>
</header>
<div class="panel">
<div class="panel-header">
<span class="panel-title">Default payment status</span>
<span class="panel-status live"></span>
</div>
<div class="panel-body">
<p class="lede">What status new payments should return.</p>
<div class="grid" id="statuses"></div>
</div>
</div>
<div class="panel">
<div class="panel-header">
<span class="panel-title">Endpoint responses</span>
<span class="panel-actions"><span class="card-desc" id="count"></span></span>
</div>
<div class="panel-body">
<div class="grid" id="endpoints"></div>
</div>
</div>
<div class="panel" id="editor" hidden>
<div class="panel-header">
<span class="panel-title">Edit response</span>
<span class="panel-status processing"></span>
</div>
<div class="panel-body">
<div class="field">
<label for="endpointDisplay">Endpoint</label>
<input id="endpointDisplay" readonly>
</div>
<div class="field">
<label for="responseJson">Mock response (JSON)</label>
<textarea id="responseJson"></textarea>
</div>
<div class="field">
<label for="statusCode">HTTP status code</label>
<input type="number" id="statusCode" value="200">
</div>
<div class="field">
<label for="delay">Delay (ms)</label>
<input type="number" id="delay" value="0">
</div>
<div class="actions">
<button class="primary" onclick="saveResponse()">Save response</button>
<button onclick="closeEditor()">Cancel</button>
</div>
<p class="note">Saving is not implemented yet — it was not implemented before this
page was rebrought onto the theme either, and pretending otherwise would be worse.</p>
</div>
</div>
<div class="panel">
<div class="panel-header"><span class="panel-title">Quick test</span></div>
<div class="panel-body">
<p class="lede">Hit this URL to get the configured responses.</p>
<div class="url">http://localhost:8006/v1/payments</div>
</div>
</div>
</div>
<script>
const STATUSES = [
{ key: 'approved', name: 'Approved', desc: 'Payment successful' },
{ key: 'rejected', name: 'Rejected', desc: 'Payment failed' },
{ key: 'pending', name: 'Pending', desc: 'Awaiting confirmation' },
{ key: 'in_process', name: 'In Process', desc: 'Being processed' },
];
const ENDPOINTS = [
{ verb: 'POST', path: '/checkout/preferences', type: 'preference', desc: 'Create payment preference (Checkout Pro)' },
{ verb: 'POST', path: '/v1/payments', type: 'payment', desc: 'Create payment (Checkout API)' },
{ verb: 'GET', path: '/v1/payments/{id}', type: 'payment_get', desc: 'Get payment details' },
{ verb: 'POST', path: '/oauth/token', type: 'oauth', desc: 'OAuth token exchange/refresh' },
];
let paymentStatus = 'approved';
let selected = null;
const $ = (id) => document.getElementById(id);
function pick(container, el) {
container.querySelectorAll('.card').forEach((c) => c.classList.remove('on'));
el.classList.add('on');
}
STATUSES.forEach((s, i) => {
const b = document.createElement('button');
b.className = 'card' + (i === 0 ? ' on' : '');
b.innerHTML = '<div class="card-name"></div><div class="card-desc"></div>';
b.firstChild.textContent = s.name;
b.lastChild.textContent = s.desc;
b.onclick = () => { paymentStatus = s.key; pick($('statuses'), b); };
$('statuses').appendChild(b);
});
ENDPOINTS.forEach((e) => {
const b = document.createElement('button');
b.className = 'card';
b.innerHTML = '<div><span class="verb ' + e.verb + '">' + e.verb + '</span>' +
'<span class="path"></span></div><div class="card-desc"></div>';
b.querySelector('.path').textContent = e.path;
b.lastChild.textContent = e.desc;
b.onclick = () => {
selected = e;
pick($('endpoints'), b);
$('editor').hidden = false;
$('endpointDisplay').value = e.verb + ' ' + e.path;
$('responseJson').value = defaultResponse(e.type);
};
$('endpoints').appendChild(b);
});
$('count').textContent = ENDPOINTS.length + ' endpoints';
function defaultResponse(type) {
const bodies = {
preference: {
id: '123456-pref-id',
init_point: 'https://www.mercadopago.com.ar/checkout/v1/redirect?pref_id=123456',
sandbox_init_point: 'https://sandbox.mercadopago.com.ar/checkout/v1/redirect?pref_id=123456',
_mock: 'MercadoPago',
},
payment: {
id: 123456,
status: paymentStatus,
status_detail: paymentStatus === 'approved' ? 'accredited' : 'cc_rejected_other_reason',
transaction_amount: 1500,
currency_id: 'ARS',
_mock: 'MercadoPago',
},
payment_get: {
id: 123456, status: 'approved', status_detail: 'accredited',
transaction_amount: 1500, _mock: 'MercadoPago',
},
oauth: {
access_token: 'APP_USR-123456-mock-token', token_type: 'Bearer',
expires_in: 15552000, refresh_token: 'TG-123456-mock-refresh', _mock: 'MercadoPago',
},
};
return JSON.stringify(bodies[type] || {}, null, 2);
}
function saveResponse() {
alert('Mock response saved (feature pending implementation)');
}
function closeEditor() {
$('editor').hidden = true;
selected = null;
document.querySelectorAll('#endpoints .card').forEach((c) => c.classList.remove('on'));
}
</script>
</body>
</html>