// Cloudflare Worker — index.js
// wrangler deploy или вставить в редактор workers.cloudflare.com
const TOCHKA_CLIENT_ID = '246527964669-38682';
const TOCHKA_CLIENT_SECRET = '84c0c392-3760-4318-8dda-654919f5b3f5';
const TOCHKA_CUSTOMER_CODE = '246527964669-38682';
const TOCHKA_TOKEN_URL = 'https://enter.tochka.com/uapi/open-banking/v1.0/oauth2/token';
const TOCHKA_PAYMENT_URL = 'https://enter.tochka.com/uapi/acquiring/v1.0/payment_operation';
/* ─── HTML ─────────────────────────────────────────────────────────────── */
const HTML = `
Оплата для клиентов
`;
/* ─── BACKEND helpers ───────────────────────────────────────────────────── */
async function getAccessToken() {
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: TOCHKA_CLIENT_ID,
client_secret: TOCHKA_CLIENT_SECRET,
scope: 'accounts',
});
const res = await fetch(TOCHKA_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token error ${res.status}: ${text}`);
}
const json = await res.json();
if (!json.access_token) {
throw new Error('Нет access_token в ответе банка');
}
return json.access_token;
}
async function createPayment(token, { amount, purpose, payerName }) {
const paymentLinkId = 'order_' + Date.now();
const body = {
amount,
customerCode: TOCHKA_CUSTOMER_CODE,
purpose: purpose + ' | ' + payerName,
paymentMode: ['card', 'sbp'],
paymentLinkId,
ttl: 60,
};
const res = await fetch(TOCHKA_PAYMENT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token,
},
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Payment error ${res.status}: ${text}`);
}
const json = await res.json();
const paymentUrl = json?.data?.paymentUrl;
if (!paymentUrl) {
throw new Error('Банк не вернул paymentUrl: ' + JSON.stringify(json));
}
return paymentUrl;
}
async function handlePayment(request) {
let body;
try {
body = await request.json();
} catch {
return jsonError('Невалидный JSON', 400);
}
const { amount, purpose, payerName, email, phone } = body;
/* Basic server-side validation */
if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) {
return jsonError('Некорректная сумма', 400);
}
if (!purpose || !payerName || !email || !phone) {
return jsonError('Заполните все поля', 400);
}
try {
const token = await getAccessToken();
const paymentUrl = await createPayment(token, { amount: Number(amount), purpose, payerName });
return jsonResponse({ paymentUrl });
} catch (err) {
return jsonError(err.message || 'Внутренняя ошибка', 502);
}
}
/* ─── Helpers ───────────────────────────────────────────────────────────── */
function jsonResponse(data, status = 200) {
return new Response(JSON.stringify(data), {
status,
headers: {
'Content-Type': 'application/json',
'X-Content-Type-Options': 'nosniff',
},
});
}
function jsonError(message, status = 500) {
return jsonResponse({ error: message }, status);
}
/* ─── Router ────────────────────────────────────────────────────────────── */
export default {
async fetch(request) {
const url = new URL(request.url);
const method = request.method.toUpperCase();
/* Serve HTML */
if (method === 'GET' && url.pathname === '/') {
return new Response(HTML, {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
},
});
}
/* Payment API */
if (method === 'POST' && url.pathname === '/api/pay') {
return handlePayment(request);
}
/* 404 */
return new Response('Not Found', { status: 404 });
},
};