Initial commit
This commit is contained in:
269
backend/routes/payment.js
Normal file
269
backend/routes/payment.js
Normal file
@@ -0,0 +1,269 @@
|
||||
const express = require('express');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const paypalService = require('../services/paypal');
|
||||
const woocommerceService = require('../services/woocommerce');
|
||||
const { generateToken, verifyToken } = require('../utils/helpers');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Validation middleware
|
||||
const validateCreateOrder = [
|
||||
body('wc_order_id').isNumeric().withMessage('WooCommerce order ID must be numeric'),
|
||||
body('total').isDecimal({ decimal_digits: '0,2' }).withMessage('Total must be a valid amount'),
|
||||
body('currency').optional().isAlpha().isLength({ min: 3, max: 3 }).withMessage('Currency must be 3 letter code'),
|
||||
];
|
||||
|
||||
const validateCaptureOrder = [
|
||||
body('paypal_order_id').notEmpty().withMessage('PayPal order ID is required'),
|
||||
body('wc_order_id').isNumeric().withMessage('WooCommerce order ID must be numeric'),
|
||||
];
|
||||
|
||||
// Create PayPal order
|
||||
router.post('/create-order', validateCreateOrder, async (req, res) => {
|
||||
try {
|
||||
// Check validation errors
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { wc_order_id, total, currency, description, items } = req.body;
|
||||
|
||||
// In development, allow bypassing WooCommerce validation
|
||||
let wcOrder = null;
|
||||
const skipWooCommerce = process.env.SKIP_WOOCOMMERCE === 'true';
|
||||
|
||||
console.log('Skip WooCommerce:', skipWooCommerce, '| Env value:', process.env.SKIP_WOOCOMMERCE);
|
||||
|
||||
if (!skipWooCommerce) {
|
||||
// Get WooCommerce order details for verification
|
||||
wcOrder = await woocommerceService.getOrder(wc_order_id);
|
||||
if (!wcOrder.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid WooCommerce order',
|
||||
details: wcOrder.error,
|
||||
suggestion: 'For testing without WooCommerce, add SKIP_WOOCOMMERCE=true to your backend/.env file'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify order total matches (security check)
|
||||
if (parseFloat(wcOrder.order.total) !== parseFloat(total)) {
|
||||
console.warn('Order total mismatch:', {
|
||||
wc_total: wcOrder.order.total,
|
||||
requested_total: total,
|
||||
order_id: wc_order_id
|
||||
});
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Order total mismatch'
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping WooCommerce validation - TEST MODE');
|
||||
}
|
||||
|
||||
// Prepare order data for PayPal
|
||||
const orderData = {
|
||||
wc_order_id,
|
||||
reference_id: `WC-${wc_order_id}-${Date.now()}`,
|
||||
total: total,
|
||||
currency: currency || 'USD',
|
||||
description: description || `Order #${wc_order_id} from ${wcOrder?.order?.billing?.first_name || 'Test Customer'}`,
|
||||
items: items || [],
|
||||
brand_name: 'Your Store Name' // Customize this
|
||||
};
|
||||
|
||||
// Create PayPal order
|
||||
const paypalResult = await paypalService.createOrder(orderData);
|
||||
|
||||
if (!paypalResult.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to create PayPal order',
|
||||
details: paypalResult.error
|
||||
});
|
||||
}
|
||||
|
||||
// Generate secure token for this transaction
|
||||
const token = generateToken({
|
||||
wc_order_id,
|
||||
paypal_order_id: paypalResult.order_id,
|
||||
total,
|
||||
created_at: Date.now()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
paypal_order_id: paypalResult.order_id,
|
||||
token: token,
|
||||
approval_url: paypalResult.links?.find(link => link.rel === 'approve')?.href
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Create Order Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Capture PayPal payment
|
||||
router.post('/capture-order', validateCaptureOrder, async (req, res) => {
|
||||
try {
|
||||
// Check validation errors
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Validation failed',
|
||||
details: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { paypal_order_id, wc_order_id, token } = req.body;
|
||||
|
||||
console.log('Capture request received:', {
|
||||
paypal_order_id,
|
||||
wc_order_id,
|
||||
hasToken: !!token,
|
||||
tokenLength: token?.length
|
||||
});
|
||||
|
||||
// Verify token (optional in development)
|
||||
const skipTokenValidation = process.env.NODE_ENV === 'development' && process.env.SKIP_TOKEN_VALIDATION === 'true';
|
||||
|
||||
if (token && !skipTokenValidation) {
|
||||
const tokenData = verifyToken(token);
|
||||
console.log('Token verification result:', {
|
||||
isValid: !!tokenData,
|
||||
tokenData: tokenData
|
||||
});
|
||||
|
||||
if (!tokenData || tokenData.wc_order_id !== parseInt(wc_order_id)) {
|
||||
console.error('Token validation failed:', {
|
||||
tokenData,
|
||||
expectedOrderId: wc_order_id,
|
||||
tokenOrderId: tokenData?.wc_order_id
|
||||
});
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid or expired token'
|
||||
});
|
||||
}
|
||||
} else if (skipTokenValidation) {
|
||||
console.warn('Skipping token validation - DEVELOPMENT MODE');
|
||||
} else {
|
||||
console.warn('No token provided in capture request');
|
||||
}
|
||||
|
||||
// Capture the PayPal payment
|
||||
const captureResult = await paypalService.captureOrder(paypal_order_id);
|
||||
|
||||
if (!captureResult.success) {
|
||||
// Mark WooCommerce order as failed only if not skipping WooCommerce
|
||||
if (process.env.SKIP_WOOCOMMERCE !== 'true') {
|
||||
await woocommerceService.failPayment(wc_order_id, captureResult.error);
|
||||
}
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Payment capture failed',
|
||||
details: captureResult.error
|
||||
});
|
||||
}
|
||||
|
||||
// Update WooCommerce order (skip if in test mode)
|
||||
let wcResult = { success: true, message: 'WooCommerce update skipped (test mode)' };
|
||||
|
||||
if (process.env.SKIP_WOOCOMMERCE !== 'true') {
|
||||
wcResult = await woocommerceService.completePayment(wc_order_id, captureResult);
|
||||
|
||||
if (!wcResult.success) {
|
||||
console.error('WooCommerce update failed after successful payment:', wcResult.error);
|
||||
// Payment succeeded but WC update failed - this needs manual review
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping WooCommerce update - TEST MODE');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
transaction_id: captureResult.transaction_id,
|
||||
status: captureResult.status,
|
||||
payer_email: captureResult.payer?.email_address,
|
||||
woocommerce_updated: wcResult.success,
|
||||
test_mode: process.env.SKIP_WOOCOMMERCE === 'true'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Capture Order Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get order status
|
||||
router.get('/order-status/:wc_order_id', async (req, res) => {
|
||||
try {
|
||||
const { wc_order_id } = req.params;
|
||||
|
||||
if (!wc_order_id || isNaN(wc_order_id)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Invalid order ID'
|
||||
});
|
||||
}
|
||||
|
||||
// Skip WooCommerce check if in test mode
|
||||
if (process.env.SKIP_WOOCOMMERCE === 'true') {
|
||||
return res.json({
|
||||
success: true,
|
||||
order_id: wc_order_id,
|
||||
status: 'completed',
|
||||
total: '0.00',
|
||||
currency: 'USD',
|
||||
payment_method: 'paypal',
|
||||
payment_method_title: 'PayPal (Test Mode)',
|
||||
test_mode: true
|
||||
});
|
||||
}
|
||||
|
||||
const wcOrder = await woocommerceService.getOrder(wc_order_id);
|
||||
|
||||
if (!wcOrder.success) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Order not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
order_id: wcOrder.order.id,
|
||||
status: wcOrder.order.status,
|
||||
total: wcOrder.order.total,
|
||||
currency: wcOrder.order.currency,
|
||||
payment_method: wcOrder.order.payment_method,
|
||||
payment_method_title: wcOrder.order.payment_method_title
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Get Order Status Error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Internal server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
173
backend/routes/webhook.js
Normal file
173
backend/routes/webhook.js
Normal file
@@ -0,0 +1,173 @@
|
||||
const express = require('express');
|
||||
const paypalService = require('../services/paypal');
|
||||
const woocommerceService = require('../services/woocommerce');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// PayPal webhook handler
|
||||
router.post('/paypal', async (req, res) => {
|
||||
try {
|
||||
const webhookBody = req.body;
|
||||
const headers = req.headers;
|
||||
|
||||
console.log('PayPal Webhook Received:', {
|
||||
event_type: webhookBody.event_type,
|
||||
resource_id: webhookBody.resource?.id,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Verify webhook signature (simplified for development)
|
||||
const isValid = paypalService.verifyWebhookSignature(headers, webhookBody, process.env.PAYPAL_WEBHOOK_ID);
|
||||
|
||||
if (!isValid) {
|
||||
console.warn('Invalid PayPal webhook signature');
|
||||
return res.status(401).json({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
// Handle different webhook events
|
||||
switch (webhookBody.event_type) {
|
||||
case 'PAYMENT.CAPTURE.COMPLETED':
|
||||
await handlePaymentCaptured(webhookBody.resource);
|
||||
break;
|
||||
|
||||
case 'PAYMENT.CAPTURE.DENIED':
|
||||
await handlePaymentDenied(webhookBody.resource);
|
||||
break;
|
||||
|
||||
case 'PAYMENT.CAPTURE.REFUNDED':
|
||||
await handlePaymentRefunded(webhookBody.resource);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unhandled PayPal webhook event:', webhookBody.event_type);
|
||||
}
|
||||
|
||||
res.status(200).json({ status: 'success' });
|
||||
|
||||
} catch (error) {
|
||||
console.error('PayPal Webhook Error:', error);
|
||||
res.status(500).json({ error: 'Webhook processing failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle payment captured
|
||||
async function handlePaymentCaptured(resource) {
|
||||
try {
|
||||
console.log('Processing payment capture webhook:', {
|
||||
capture_id: resource.id,
|
||||
amount: resource.amount?.value,
|
||||
custom_id: resource.custom_id
|
||||
});
|
||||
|
||||
const customId = resource.custom_id;
|
||||
if (!customId) {
|
||||
console.warn('No custom_id found in payment capture');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract WooCommerce order ID from custom_id
|
||||
const wcOrderId = parseInt(customId);
|
||||
if (isNaN(wcOrderId)) {
|
||||
console.warn('Invalid WooCommerce order ID in custom_id:', customId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare payment data
|
||||
const paymentData = {
|
||||
transaction_id: resource.id,
|
||||
payer: {
|
||||
email_address: resource.payer?.email_address || 'N/A'
|
||||
}
|
||||
};
|
||||
|
||||
// Update WooCommerce order
|
||||
const result = await woocommerceService.completePayment(wcOrderId, paymentData);
|
||||
|
||||
if (result.success) {
|
||||
console.log('Payment capture webhook processed successfully');
|
||||
} else {
|
||||
console.error('Failed to update WooCommerce order from webhook:', result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Handle Payment Captured Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle payment denied
|
||||
async function handlePaymentDenied(resource) {
|
||||
try {
|
||||
console.log('Processing payment denied webhook:', {
|
||||
capture_id: resource.id,
|
||||
custom_id: resource.custom_id
|
||||
});
|
||||
|
||||
const customId = resource.custom_id;
|
||||
if (!customId) {
|
||||
console.warn('No custom_id found in payment denial');
|
||||
return;
|
||||
}
|
||||
|
||||
const wcOrderId = parseInt(customId);
|
||||
if (isNaN(wcOrderId)) {
|
||||
console.warn('Invalid WooCommerce order ID in custom_id:', customId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark order as failed
|
||||
const result = await woocommerceService.failPayment(wcOrderId, 'Payment was denied by PayPal');
|
||||
|
||||
if (result.success) {
|
||||
console.log('Payment denial webhook processed successfully');
|
||||
} else {
|
||||
console.error('Failed to update WooCommerce order from denial webhook:', result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Handle Payment Denied Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle payment refunded
|
||||
async function handlePaymentRefunded(resource) {
|
||||
try {
|
||||
console.log('Processing payment refund webhook:', {
|
||||
refund_id: resource.id,
|
||||
amount: resource.amount?.value,
|
||||
custom_id: resource.custom_id
|
||||
});
|
||||
|
||||
const customId = resource.custom_id;
|
||||
if (!customId) {
|
||||
console.warn('No custom_id found in payment refund');
|
||||
return;
|
||||
}
|
||||
|
||||
const wcOrderId = parseInt(customId);
|
||||
if (isNaN(wcOrderId)) {
|
||||
console.warn('Invalid WooCommerce order ID in custom_id:', customId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add refund note to WooCommerce order
|
||||
const noteText = `Payment refunded via PayPal. Refund ID: ${resource.id}. Amount: ${resource.amount?.value} ${resource.amount?.currency_code}`;
|
||||
|
||||
await woocommerceService.addOrderNote(wcOrderId, noteText, true);
|
||||
|
||||
console.log('Payment refund webhook processed successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Handle Payment Refunded Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Health check for webhooks
|
||||
router.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'OK',
|
||||
service: 'webhooks',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user