Skip to main content

Интеграция платежных систем

Подробное руководство по подключению и настройке различных платежных систем.

Stripe

Базовая настройка

const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

// Создание Payment Intent
app.post('/api/payments/create-intent', async (req, res) => {
try {
const { amount, currency, marketplace_id } = req.body;

const paymentIntent = await stripe.paymentIntents.create({
amount: amount * 100, // Stripe использует центы
currency: currency.toLowerCase(),
metadata: {
marketplace_id: marketplace_id,
order_id: req.body.order_id
},
application_fee_amount: Math.round(amount * 100 * 0.05), // 5% комиссия платформы
transfer_data: {
destination: req.body.seller_stripe_account_id
}
});

res.json({
client_secret: paymentIntent.client_secret,
payment_intent_id: paymentIntent.id
});

} catch (error) {
console.error('Stripe payment intent creation failed:', error);
res.status(400).json({ error: error.message });
}
});

// Webhook для обработки событий
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];

try {
const event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);

switch (event.type) {
case 'payment_intent.succeeded':
handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
handlePaymentFailure(event.data.object);
break;
case 'account.updated':
handleAccountUpdate(event.data.object);
break;
}

res.json({received: true});
} catch (err) {
console.error('Stripe webhook error:', err);
res.status(400).send(`Webhook Error: ${err.message}`);
}
});

Фронтенд интеграция

// Stripe Elements на фронтенде
import { loadStripe } from '@stripe/stripe-js';
import {
Elements,
CardElement,
useStripe,
useElements
} from '@stripe/react-stripe-js';

const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);

function CheckoutForm({ orderData }) {
const stripe = useStripe();
const elements = useElements();
const [processing, setProcessing] = useState(false);

const handleSubmit = async (event) => {
event.preventDefault();

if (!stripe || !elements) return;

setProcessing(true);

try {
// Создаем Payment Intent на сервере
const response = await fetch('/api/payments/create-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: orderData.total,
currency: orderData.currency,
marketplace_id: orderData.marketplace_id,
order_id: orderData.id
})
});

const { client_secret } = await response.json();

// Подтверждаем платеж
const result = await stripe.confirmCardPayment(client_secret, {
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: orderData.customer.name,
email: orderData.customer.email
}
}
});

if (result.error) {
console.error('Payment failed:', result.error);
setError(result.error.message);
} else {
console.log('Payment succeeded:', result.paymentIntent);
// Редирект на страницу успеха
window.location.href = `/order/${orderData.id}/success`;
}

} catch (error) {
console.error('Payment processing error:', error);
setError('Произошла ошибка при обработке платежа');
} finally {
setProcessing(false);
}
};

return (
<form onSubmit={handleSubmit}>
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#424770',
'::placeholder': {
color: '#aab7c4',
},
},
},
}}
/>
<button
type="submit"
disabled={!stripe || processing}
className="btn btn-primary mt-4"
>
{processing ? 'Обработка...' : `Оплатить ${orderData.total} ${orderData.currency}`}
</button>
</form>
);
}

function PaymentPage({ orderData }) {
return (
<Elements stripe={stripePromise}>
<CheckoutForm orderData={orderData} />
</Elements>
);
}

PayPal

Серверная интеграция

const paypal = require('@paypal/checkout-server-sdk');

// Настройка PayPal SDK
function environment() {
const clientId = process.env.PAYPAL_CLIENT_ID;
const clientSecret = process.env.PAYPAL_CLIENT_SECRET;

return process.env.NODE_ENV === 'production'
? new paypal.core.LiveEnvironment(clientId, clientSecret)
: new paypal.core.SandboxEnvironment(clientId, clientSecret);
}

const client = new paypal.core.PayPalHttpClient(environment());

// Создание заказа PayPal
app.post('/api/payments/paypal/create', async (req, res) => {
try {
const { amount, currency, order_id, marketplace_id } = req.body;

const request = new paypal.orders.OrdersCreateRequest();
request.prefer("return=representation");
request.requestBody({
intent: 'CAPTURE',
purchase_units: [{
amount: {
currency_code: currency,
value: amount.toString()
},
custom_id: order_id,
description: `Заказ #${order_id} на маркетплейсе`
}],
application_context: {
return_url: `${process.env.FRONTEND_URL}/payment/success`,
cancel_url: `${process.env.FRONTEND_URL}/payment/cancel`,
brand_name: 'Marketplace Platform',
locale: 'ru-RU',
landing_page: 'BILLING',
user_action: 'PAY_NOW'
}
});

const order = await client.execute(request);

res.json({
order_id: order.result.id,
approval_url: order.result.links.find(link => link.rel === 'approve').href
});

} catch (error) {
console.error('PayPal order creation failed:', error);
res.status(400).json({ error: error.message });
}
});

// Подтверждение платежа
app.post('/api/payments/paypal/capture/:orderId', async (req, res) => {
try {
const request = new paypal.orders.OrdersCaptureRequest(req.params.orderId);
request.requestBody({});

const capture = await client.execute(request);

if (capture.result.status === 'COMPLETED') {
await handlePaymentSuccess({
payment_method: 'paypal',
transaction_id: capture.result.id,
order_id: capture.result.purchase_units[0].custom_id,
amount: parseFloat(capture.result.purchase_units[0].amount.value),
currency: capture.result.purchase_units[0].amount.currency_code
});
}

res.json(capture.result);

} catch (error) {
console.error('PayPal capture failed:', error);
res.status(400).json({ error: error.message });
}
});

PayPal фронтенд

// PayPal кнопки
import { PayPalScriptProvider, PayPalButtons } from "@paypal/react-paypal-js";

function PayPalCheckout({ orderData }) {
const initialOptions = {
"client-id": process.env.REACT_APP_PAYPAL_CLIENT_ID,
currency: orderData.currency,
intent: "capture",
locale: "ru_RU"
};

const createOrder = async () => {
try {
const response = await fetch('/api/payments/paypal/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: orderData.total,
currency: orderData.currency,
order_id: orderData.id,
marketplace_id: orderData.marketplace_id
})
});

const data = await response.json();
return data.order_id;

} catch (error) {
console.error('PayPal order creation failed:', error);
}
};

const onApprove = async (data) => {
try {
const response = await fetch(`/api/payments/paypal/capture/${data.orderID}`, {
method: 'POST'
});

const details = await response.json();

if (details.status === 'COMPLETED') {
window.location.href = `/order/${orderData.id}/success`;
}

} catch (error) {
console.error('PayPal approval failed:', error);
}
};

return (
<PayPalScriptProvider options={initialOptions}>
<PayPalButtons
createOrder={createOrder}
onApprove={onApprove}
onError={(err) => {
console.error('PayPal button error:', err);
}}
style={{
layout: "vertical",
color: "blue",
shape: "rect",
label: "paypal"
}}
/>
</PayPalScriptProvider>
);
}

ЮKassa (Yandex Checkout)

Серверная интеграция

const { YooCheckout } = require('@a2seven/yoo-checkout');

const checkout = new YooCheckout({
shopId: process.env.YOOKASSA_SHOP_ID,
secretKey: process.env.YOOKASSA_SECRET_KEY
});

// Создание платежа
app.post('/api/payments/yookassa/create', async (req, res) => {
try {
const { amount, currency, order_id, customer } = req.body;

const payment = await checkout.createPayment({
amount: {
value: amount.toFixed(2),
currency: currency
},
capture: true,
confirmation: {
type: 'redirect',
return_url: `${process.env.FRONTEND_URL}/payment/success?order_id=${order_id}`
},
description: `Заказ #${order_id}`,
metadata: {
order_id: order_id,
marketplace_id: req.body.marketplace_id
},
receipt: {
customer: {
email: customer.email,
phone: customer.phone
},
items: req.body.items.map(item => ({
description: item.name,
quantity: item.quantity.toString(),
amount: {
value: item.price.toFixed(2),
currency: currency
},
vat_code: 1, // НДС 20%
payment_mode: 'full_payment',
payment_subject: 'commodity'
}))
}
});

res.json({
payment_id: payment.id,
confirmation_url: payment.confirmation.confirmation_url
});

} catch (error) {
console.error('YooKassa payment creation failed:', error);
res.status(400).json({ error: error.message });
}
});

// Webhook для YooKassa
app.post('/webhooks/yookassa', async (req, res) => {
try {
const notification = req.body;

// Проверка подписи
const isValid = checkout.validateNotification(notification, req.headers['x-yookassa-signature']);
if (!isValid) {
return res.status(400).send('Invalid signature');
}

const payment = notification.object;

if (payment.status === 'succeeded') {
await handlePaymentSuccess({
payment_method: 'yookassa',
transaction_id: payment.id,
order_id: payment.metadata.order_id,
amount: parseFloat(payment.amount.value),
currency: payment.amount.currency
});
} else if (payment.status === 'canceled') {
await handlePaymentFailure({
payment_method: 'yookassa',
transaction_id: payment.id,
order_id: payment.metadata.order_id,
error: payment.cancellation_details?.reason
});
}

res.status(200).send('OK');

} catch (error) {
console.error('YooKassa webhook error:', error);
res.status(400).send('Webhook processing failed');
}
});

Обработка платежей

Универсальная обработка успешных платежей

async function handlePaymentSuccess(paymentData) {
const transaction = await db.beginTransaction();

try {
// 1. Обновляем статус заказа
await db.query(`
UPDATE orders
SET status = 'paid', payment_method = $1, transaction_id = $2, paid_at = NOW()
WHERE id = $3
`, [paymentData.payment_method, paymentData.transaction_id, paymentData.order_id], { transaction });

// 2. Создаем запись о платеже
await db.query(`
INSERT INTO payments (order_id, amount, currency, payment_method, transaction_id, status, created_at)
VALUES ($1, $2, $3, $4, $5, 'completed', NOW())
`, [
paymentData.order_id,
paymentData.amount,
paymentData.currency,
paymentData.payment_method,
paymentData.transaction_id
], { transaction });

// 3. Рассчитываем и начисляем комиссии
const order = await db.query(
'SELECT * FROM orders WHERE id = $1',
[paymentData.order_id],
{ transaction }
);

const platformFee = order.rows[0].total * 0.05; // 5% комиссия платформы
const sellerAmount = order.rows[0].total - platformFee;

// 4. Создаем записи о транзакциях
await db.query(`
INSERT INTO financial_transactions (
order_id, user_id, type, amount, currency, description, created_at
) VALUES
($1, $2, 'platform_fee', $3, $4, 'Комиссия платформы', NOW()),
($1, $5, 'seller_payment', $6, $4, 'Оплата за товар', NOW())
`, [
paymentData.order_id,
1, // ID платформы
platformFee,
paymentData.currency,
order.rows[0].seller_id,
sellerAmount
], { transaction });

// 5. Обновляем остатки на счетах
await db.query(`
UPDATE user_balances
SET balance = balance + $1
WHERE user_id = $2 AND currency = $3
`, [sellerAmount, order.rows[0].seller_id, paymentData.currency], { transaction });

// 6. Уменьшаем остатки товаров
const orderItems = await db.query(
'SELECT product_id, quantity FROM order_items WHERE order_id = $1',
[paymentData.order_id],
{ transaction }
);

for (const item of orderItems.rows) {
await db.query(`
UPDATE products
SET stock_quantity = stock_quantity - $1
WHERE id = $2
`, [item.quantity, item.product_id], { transaction });
}

await transaction.commit();

// 7. Отправляем уведомления
await sendPaymentNotifications(paymentData.order_id);

// 8. Логируем успешный платеж
console.log(`Payment successful: Order ${paymentData.order_id}, Amount: ${paymentData.amount} ${paymentData.currency}`);

} catch (error) {
await transaction.rollback();
console.error('Payment processing failed:', error);
throw error;
}
}

Обработка неуспешных платежей

async function handlePaymentFailure(paymentData) {
try {
// 1. Обновляем статус заказа
await db.query(`
UPDATE orders
SET status = 'payment_failed', payment_error = $1, updated_at = NOW()
WHERE id = $2
`, [paymentData.error, paymentData.order_id]);

// 2. Создаем запись о неуспешном платеже
await db.query(`
INSERT INTO payments (order_id, payment_method, transaction_id, status, error_message, created_at)
VALUES ($1, $2, $3, 'failed', $4, NOW())
`, [
paymentData.order_id,
paymentData.payment_method,
paymentData.transaction_id,
paymentData.error
]);

// 3. Возвращаем товары в резерв
await db.query(`
UPDATE products p
SET reserved_quantity = reserved_quantity - oi.quantity
FROM order_items oi
WHERE oi.order_id = $1 AND oi.product_id = p.id
`, [paymentData.order_id]);

// 4. Отправляем уведомление о неуспешном платеже
await sendPaymentFailureNotification(paymentData.order_id);

console.log(`Payment failed: Order ${paymentData.order_id}, Error: ${paymentData.error}`);

} catch (error) {
console.error('Payment failure processing error:', error);
}
}

Возвраты и отмены

Возврат платежа

app.post('/api/payments/refund', async (req, res) => {
try {
const { order_id, amount, reason } = req.body;

// Получаем информацию о платеже
const payment = await db.query(
'SELECT * FROM payments WHERE order_id = $1 AND status = $2',
[order_id, 'completed']
);

if (payment.rows.length === 0) {
return res.status(404).json({ error: 'Payment not found' });
}

const paymentData = payment.rows[0];
let refund;

// Выполняем возврат в зависимости от платежной системы
switch (paymentData.payment_method) {
case 'stripe':
refund = await stripe.refunds.create({
payment_intent: paymentData.transaction_id,
amount: amount ? amount * 100 : undefined, // Частичный или полный возврат
reason: 'requested_by_customer',
metadata: {
order_id: order_id,
reason: reason
}
});
break;

case 'paypal':
// PayPal возврат
const refundRequest = new paypal.payments.CapturesRefundRequest(paymentData.transaction_id);
refundRequest.requestBody({
amount: amount ? {
value: amount.toString(),
currency_code: paymentData.currency
} : undefined
});
refund = await client.execute(refundRequest);
break;

case 'yookassa':
refund = await checkout.createRefund({
payment_id: paymentData.transaction_id,
amount: amount ? {
value: amount.toFixed(2),
currency: paymentData.currency
} : undefined
});
break;

default:
throw new Error(`Refund not supported for payment method: ${paymentData.payment_method}`);
}

// Сохраняем информацию о возврате
await db.query(`
INSERT INTO refunds (order_id, payment_id, amount, currency, refund_id, reason, status, created_at)
VALUES ($1, $2, $3, $4, $5, $6, 'pending', NOW())
`, [
order_id,
paymentData.id,
amount || paymentData.amount,
paymentData.currency,
refund.id,
reason
]);

res.json({
refund_id: refund.id,
status: 'pending',
message: 'Refund initiated successfully'
});

} catch (error) {
console.error('Refund processing failed:', error);
res.status(400).json({ error: error.message });
}
});

Безопасность платежей

Проверка подписей webhook'ов

// Middleware для проверки подписей
function verifyWebhookSignature(provider) {
return (req, res, next) => {
try {
let isValid = false;

switch (provider) {
case 'stripe':
const sig = req.headers['stripe-signature'];
stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
isValid = true;
break;

case 'yookassa':
const signature = req.headers['x-yookassa-signature'];
isValid = checkout.validateNotification(req.body, signature);
break;

case 'paypal':
// PayPal использует OAuth для верификации
isValid = true; // Дополнительная проверка через PayPal SDK
break;
}

if (!isValid) {
return res.status(400).send('Invalid signature');
}

next();
} catch (error) {
console.error('Webhook signature verification failed:', error);
res.status(400).send('Signature verification failed');
}
};
}

// Использование middleware
app.post('/webhooks/stripe',
express.raw({type: 'application/json'}),
verifyWebhookSignature('stripe'),
handleStripeWebhook
);

Rate limiting для платежных API

const rateLimit = require('express-rate-limit');

// Rate limiting для платежных операций
const paymentLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 5, // максимум 5 попыток платежа за окно
message: 'Too many payment attempts, please try again later',
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
console.log(`Payment rate limit exceeded for IP: ${req.ip}`);
res.status(429).json({
error: 'Too many payment attempts',
retryAfter: Math.round(req.rateLimit.msBeforeNext / 1000)
});
}
});

app.use('/api/payments/', paymentLimiter);

Мониторинг и аналитика

Дашборд платежей

// API для аналитики платежей
app.get('/api/admin/payments/analytics', async (req, res) => {
try {
const { start_date, end_date, marketplace_id } = req.query;

const analytics = await db.query(`
SELECT
DATE(created_at) as date,
payment_method,
COUNT(*) as transaction_count,
SUM(amount) as total_amount,
SUM(CASE WHEN status = 'completed' THEN amount ELSE 0 END) as successful_amount,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_count,
AVG(amount) as average_amount
FROM payments p
JOIN orders o ON p.order_id = o.id
WHERE p.created_at BETWEEN $1 AND $2
${marketplace_id ? 'AND o.marketplace_id = $3' : ''}
GROUP BY DATE(created_at), payment_method
ORDER BY date DESC, payment_method
`, marketplace_id ? [start_date, end_date, marketplace_id] : [start_date, end_date]);

res.json({
analytics: analytics.rows,
summary: {
total_transactions: analytics.rows.reduce((sum, row) => sum + parseInt(row.transaction_count), 0),
total_revenue: analytics.rows.reduce((sum, row) => sum + parseFloat(row.successful_amount), 0),
total_failed: analytics.rows.reduce((sum, row) => sum + parseInt(row.failed_count), 0)
}
});

} catch (error) {
console.error('Payment analytics error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});