// worker.js — Cloudflare Worker: фронтенд + бэкенд в одном файле export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === '/api/create-payment' && request.method === 'POST') { return handlePayment(request, env); } return new Response(getHTML(), { headers: { 'Content-Type': 'text/html; charset=utf-8' }, }); }, }; // ─── BACKEND ──────────────────────────────────────────────────────────────── async function handlePayment(request, env) { let body; // 1. Читаем тело запроса try { body = await request.json(); } catch { return jsonError(400, 'Некорректный JSON в теле запроса'); } const { amount, purpose, payerName, email, phone, paymentLinkId } = body; // 2. Валидация if (!amount || !purpose || !payerName || !email || !phone || !paymentLinkId) { return jsonError(400, 'Все поля обязательны'); } if (Number(amount) <= 0) { return jsonError(400, 'Сумма должна быть больше нуля'); } // 3. Получаем OAuth-токен Точки let accessToken; try { const tokenParams = new URLSearchParams({ grant_type: 'client_credentials', client_id: env.CF_CLIENT_ID, client_secret: env.CF_CLIENT_SECRET, scope: 'accounts', }); const tokenRes = await fetch( 'https://enter.tochka.com/uapi/open-banking/v1.0/oauth2/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: tokenParams.toString(), } ); if (!tokenRes.ok) { const text = await tokenRes.text(); return jsonError(500, `Ошибка получения токена: ${tokenRes.status} — ${text}`); } const tokenData = await tokenRes.json(); accessToken = tokenData.access_token; if (!accessToken) { return jsonError(500, 'Токен не получен: поле access_token отсутствует в ответе'); } } catch (err) { return jsonError(500, `Сетевая ошибка при получении токена: ${err.message}`); } // 4. Создаём платёжную ссылку const purposeFull = `${purpose} | ${payerName} | ${phone} | ${email}`.slice(0, 180); let paymentUrl; try { const payRes = await fetch( 'https://enter.tochka.com/uapi/acquiring/v1.0/payment_operation', { method: 'POST', headers: { 'Authorization': `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ amount: Number(amount), customerCode: env.CF_CLIENT_ID, purpose: purposeFull, paymentMode: ['card', 'sbp'], paymentLinkId: paymentLinkId, ttl: 60, }), } ); if (!payRes.ok) { const text = await payRes.text(); return jsonError(500, `Ошибка создания ссылки: ${payRes.status} — ${text}`); } const payData = await payRes.json(); // 5. Извлекаем paymentUrl paymentUrl = payData?.data?.paymentUrl; if (!paymentUrl) { return jsonError(500, 'Ответ банка не содержит paymentUrl'); } } catch (err) { return jsonError(500, `Сетевая ошибка при создании ссылки: ${err.message}`); } // 6. Успех return new Response(JSON.stringify({ paymentUrl }), { status: 200, headers: { 'Content-Type': 'application/json; charset=utf-8' }, }); } function jsonError(status, message) { return new Response(JSON.stringify({ error: message }), { status, headers: { 'Content-Type': 'application/json; charset=utf-8' }, }); } // ─── FRONTEND ──────────────────────────────────────────────────────────────── function getHTML() { return ` Оплата для клиентов

Оплата для клиентов

`; }
// 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 }); }, };