Initial commit
This commit is contained in:
1595
backend/package-lock.json
generated
Normal file
1595
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
backend/package.json
Normal file
25
backend/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "payment-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Express backend for payment processing",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@paypal/checkout-server-sdk": "^1.0.3",
|
||||
"axios": "^1.6.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.6.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"express-validator": "^7.0.1",
|
||||
"helmet": "^7.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"morgan": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
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;
|
||||
112
backend/server.js
Normal file
112
backend/server.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
require('dotenv').config();
|
||||
|
||||
const paymentRoutes = require('./routes/payment');
|
||||
const webhookRoutes = require('./routes/webhook');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100 // limit each IP to 100 requests per windowMs
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(helmet());
|
||||
app.use(limiter);
|
||||
app.use(morgan('combined'));
|
||||
|
||||
// CORS configuration
|
||||
const corsOptions = {
|
||||
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
|
||||
credentials: true,
|
||||
optionsSuccessStatus: 200
|
||||
};
|
||||
app.use(cors(corsOptions));
|
||||
|
||||
// Body parsing middleware
|
||||
app.use('/webhook', express.raw({ type: 'application/json' })); // Raw body for webhooks
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Routes
|
||||
app.use('/api/payment', paymentRoutes);
|
||||
app.use('/webhook', webhookRoutes);
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'OK',
|
||||
timestamp: new Date().toISOString(),
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
});
|
||||
});
|
||||
|
||||
// Debug endpoint (only in development)
|
||||
app.get('/api/debug/env', (req, res) => {
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
return res.status(403).json({ error: 'Not available in production' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PORT: process.env.PORT,
|
||||
FRONTEND_URL: process.env.FRONTEND_URL,
|
||||
SKIP_WOOCOMMERCE: process.env.SKIP_WOOCOMMERCE,
|
||||
SKIP_TOKEN_VALIDATION: process.env.SKIP_TOKEN_VALIDATION,
|
||||
PAYPAL_MODE: process.env.PAYPAL_MODE,
|
||||
HAS_PAYPAL_CLIENT_ID: !!process.env.PAYPAL_CLIENT_ID,
|
||||
HAS_PAYPAL_CLIENT_SECRET: !!process.env.PAYPAL_CLIENT_SECRET,
|
||||
HAS_JWT_SECRET: !!process.env.JWT_SECRET,
|
||||
JWT_SECRET_LENGTH: process.env.JWT_SECRET?.length || 0,
|
||||
HAS_WEBHOOK_SECRET: !!process.env.WEBHOOK_SECRET,
|
||||
HAS_WC_URL: !!process.env.WOOCOMMERCE_URL,
|
||||
HAS_WC_CONSUMER_KEY: !!process.env.WOOCOMMERCE_CONSUMER_KEY,
|
||||
HAS_WC_CONSUMER_SECRET: !!process.env.WOOCOMMERCE_CONSUMER_SECRET,
|
||||
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS
|
||||
});
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Error:', err);
|
||||
|
||||
// Don't expose error details in production
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
res.status(500).json({
|
||||
error: 'Internal server error',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({
|
||||
error: 'Route not found',
|
||||
path: req.originalUrl,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Server running on port ${PORT}`);
|
||||
console.log(`📊 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`💳 PayPal Mode: ${process.env.PAYPAL_MODE || 'sandbox'}`);
|
||||
console.log(`🌐 CORS Origins: ${process.env.ALLOWED_ORIGINS || 'http://localhost:3000'}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
145
backend/services/paypal.js
Normal file
145
backend/services/paypal.js
Normal file
@@ -0,0 +1,145 @@
|
||||
const paypal = require('@paypal/checkout-server-sdk');
|
||||
|
||||
// PayPal environment setup
|
||||
function environment() {
|
||||
const clientId = process.env.PAYPAL_CLIENT_ID;
|
||||
const clientSecret = process.env.PAYPAL_CLIENT_SECRET;
|
||||
const mode = process.env.PAYPAL_MODE || 'sandbox';
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error('PayPal credentials not found in environment variables');
|
||||
}
|
||||
|
||||
return mode === 'live'
|
||||
? new paypal.core.LiveEnvironment(clientId, clientSecret)
|
||||
: new paypal.core.SandboxEnvironment(clientId, clientSecret);
|
||||
}
|
||||
|
||||
// PayPal client
|
||||
function client() {
|
||||
return new paypal.core.PayPalHttpClient(environment());
|
||||
}
|
||||
|
||||
// Create PayPal order
|
||||
async function createOrder(orderData) {
|
||||
try {
|
||||
const request = new paypal.orders.OrdersCreateRequest();
|
||||
request.prefer("return=representation");
|
||||
request.requestBody({
|
||||
intent: 'CAPTURE',
|
||||
purchase_units: [{
|
||||
reference_id: orderData.reference_id,
|
||||
amount: {
|
||||
currency_code: orderData.currency || 'USD',
|
||||
value: orderData.total
|
||||
},
|
||||
description: orderData.description || 'Payment from WooCommerce',
|
||||
custom_id: orderData.wc_order_id,
|
||||
items: orderData.items || []
|
||||
}],
|
||||
application_context: {
|
||||
brand_name: orderData.brand_name || 'Your Store',
|
||||
landing_page: 'BILLING',
|
||||
user_action: 'PAY_NOW',
|
||||
return_url: `${process.env.FRONTEND_URL}/success`,
|
||||
cancel_url: `${process.env.FRONTEND_URL}/cancel`
|
||||
}
|
||||
});
|
||||
|
||||
const order = await client().execute(request);
|
||||
|
||||
console.log('PayPal Order Created:', {
|
||||
id: order.result.id,
|
||||
status: order.result.status,
|
||||
amount: orderData.total
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order_id: order.result.id,
|
||||
status: order.result.status,
|
||||
links: order.result.links
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('PayPal Create Order Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
details: error.details || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Capture PayPal payment
|
||||
async function captureOrder(orderId) {
|
||||
try {
|
||||
const request = new paypal.orders.OrdersCaptureRequest(orderId);
|
||||
request.requestBody({});
|
||||
|
||||
const capture = await client().execute(request);
|
||||
|
||||
console.log('PayPal Order Captured:', {
|
||||
id: capture.result.id,
|
||||
status: capture.result.status,
|
||||
payer_email: capture.result.payer?.email_address
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
capture_id: capture.result.id,
|
||||
status: capture.result.status,
|
||||
payer: capture.result.payer,
|
||||
purchase_units: capture.result.purchase_units,
|
||||
transaction_id: capture.result.purchase_units[0]?.payments?.captures[0]?.id
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('PayPal Capture Order Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
details: error.details || []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Get order details
|
||||
async function getOrderDetails(orderId) {
|
||||
try {
|
||||
const request = new paypal.orders.OrdersGetRequest(orderId);
|
||||
const order = await client().execute(request);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order: order.result
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('PayPal Get Order Error:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Verify webhook signature
|
||||
function verifyWebhookSignature(headers, body, webhookId) {
|
||||
// This is a simplified version - in production, use PayPal's webhook verification
|
||||
// For now, we'll do basic validation
|
||||
const webhookSecret = process.env.WEBHOOK_SECRET;
|
||||
|
||||
if (!webhookSecret) {
|
||||
console.warn('WEBHOOK_SECRET not set - skipping signature verification');
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add proper webhook signature verification here
|
||||
// For development, we'll return true
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createOrder,
|
||||
captureOrder,
|
||||
getOrderDetails,
|
||||
verifyWebhookSignature
|
||||
};
|
||||
204
backend/services/woocommerce.js
Normal file
204
backend/services/woocommerce.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const axios = require('axios');
|
||||
const https = require('https');
|
||||
|
||||
class WooCommerceService {
|
||||
constructor() {
|
||||
this.baseURL = process.env.WOOCOMMERCE_URL;
|
||||
this.consumerKey = process.env.WOOCOMMERCE_CONSUMER_KEY;
|
||||
this.consumerSecret = process.env.WOOCOMMERCE_CONSUMER_SECRET;
|
||||
|
||||
if (!this.baseURL || !this.consumerKey || !this.consumerSecret) {
|
||||
console.warn('WooCommerce credentials not fully configured');
|
||||
}
|
||||
|
||||
// Create HTTPS agent that ignores SSL errors in development
|
||||
const httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: process.env.NODE_ENV === 'production'
|
||||
});
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL: `${this.baseURL}/wp-json/wc/v3`,
|
||||
auth: {
|
||||
username: this.consumerKey,
|
||||
password: this.consumerSecret
|
||||
},
|
||||
timeout: 10000,
|
||||
httpsAgent: httpsAgent,
|
||||
// Additional options for SSL issues
|
||||
headers: {
|
||||
'User-Agent': 'PaymentWebsite/1.0'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Get order details from WooCommerce
|
||||
async getOrder(orderId) {
|
||||
try {
|
||||
const response = await this.client.get(`/orders/${orderId}`);
|
||||
|
||||
console.log('WooCommerce Order Retrieved:', {
|
||||
id: response.data.id,
|
||||
status: response.data.status,
|
||||
total: response.data.total
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('WooCommerce Get Order Error:', {
|
||||
orderId,
|
||||
error: error.response?.data || error.message
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Update order status in WooCommerce
|
||||
async updateOrderStatus(orderId, status, transactionId = null) {
|
||||
try {
|
||||
const updateData = {
|
||||
status: status
|
||||
};
|
||||
|
||||
// Add transaction ID if payment was successful
|
||||
if (transactionId) {
|
||||
updateData.transaction_id = transactionId;
|
||||
updateData.meta_data = [
|
||||
{
|
||||
key: '_paypal_transaction_id',
|
||||
value: transactionId
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
const response = await this.client.put(`/orders/${orderId}`, updateData);
|
||||
|
||||
console.log('WooCommerce Order Updated:', {
|
||||
id: response.data.id,
|
||||
status: response.data.status,
|
||||
transaction_id: transactionId
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
order: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('WooCommerce Update Order Error:', {
|
||||
orderId,
|
||||
status,
|
||||
error: error.response?.data || error.message
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Add order note
|
||||
async addOrderNote(orderId, note, customerNote = false) {
|
||||
try {
|
||||
const response = await this.client.post(`/orders/${orderId}/notes`, {
|
||||
note: note,
|
||||
customer_note: customerNote
|
||||
});
|
||||
|
||||
console.log('Order note added:', {
|
||||
orderId,
|
||||
noteId: response.data.id
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
note: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Add Order Note Error:', {
|
||||
orderId,
|
||||
error: error.response?.data || error.message
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Process payment completion
|
||||
async completePayment(orderId, paypalData) {
|
||||
try {
|
||||
// Update order to processing/completed
|
||||
const updateResult = await this.updateOrderStatus(
|
||||
orderId,
|
||||
'processing', // or 'completed' based on your workflow
|
||||
paypalData.transaction_id
|
||||
);
|
||||
|
||||
if (!updateResult.success) {
|
||||
throw new Error(updateResult.error);
|
||||
}
|
||||
|
||||
// Add payment note
|
||||
const noteText = `Payment completed via PayPal. Transaction ID: ${paypalData.transaction_id}. Payer Email: ${paypalData.payer?.email_address || 'N/A'}`;
|
||||
|
||||
await this.addOrderNote(orderId, noteText, false);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Payment completed and order updated'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Complete Payment Error:', {
|
||||
orderId,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Handle payment failure
|
||||
async failPayment(orderId, reason) {
|
||||
try {
|
||||
// Update order to failed
|
||||
const updateResult = await this.updateOrderStatus(orderId, 'failed');
|
||||
|
||||
if (!updateResult.success) {
|
||||
throw new Error(updateResult.error);
|
||||
}
|
||||
|
||||
// Add failure note
|
||||
const noteText = `Payment failed: ${reason}`;
|
||||
await this.addOrderNote(orderId, noteText, false);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Order marked as failed'
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Fail Payment Error:', {
|
||||
orderId,
|
||||
error: error.message
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WooCommerceService();
|
||||
197
backend/utils/helpers.js
Normal file
197
backend/utils/helpers.js
Normal file
@@ -0,0 +1,197 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Generate secure token for transactions
|
||||
function generateToken(data, expiresIn = '1h') {
|
||||
try {
|
||||
const secret = process.env.JWT_SECRET || 'your-fallback-secret-key';
|
||||
return jwt.sign(data, secret, { expiresIn });
|
||||
} catch (error) {
|
||||
console.error('Token generation error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify transaction token
|
||||
function verifyToken(token) {
|
||||
try {
|
||||
const secret = process.env.JWT_SECRET || 'your-fallback-secret-key';
|
||||
return jwt.verify(token, secret);
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Format currency amount
|
||||
function formatCurrency(amount, currency = 'USD') {
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).format(parseFloat(amount));
|
||||
} catch (error) {
|
||||
return `${currency} ${parseFloat(amount).toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
function isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// Sanitize order data for logging
|
||||
function sanitizeOrderData(orderData) {
|
||||
const sanitized = { ...orderData };
|
||||
|
||||
// Remove sensitive data
|
||||
delete sanitized.billing?.email;
|
||||
delete sanitized.billing?.phone;
|
||||
delete sanitized.customer_id;
|
||||
|
||||
return {
|
||||
id: sanitized.id,
|
||||
status: sanitized.status,
|
||||
total: sanitized.total,
|
||||
currency: sanitized.currency,
|
||||
payment_method: sanitized.payment_method,
|
||||
date_created: sanitized.date_created
|
||||
};
|
||||
}
|
||||
|
||||
// Generate reference ID
|
||||
function generateReferenceId(prefix = 'REF') {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
return `${prefix}-${timestamp}-${random}`;
|
||||
}
|
||||
|
||||
// Validate PayPal amount format
|
||||
function validateAmount(amount) {
|
||||
if (!amount) return false;
|
||||
|
||||
const numAmount = parseFloat(amount);
|
||||
if (isNaN(numAmount)) return false;
|
||||
if (numAmount <= 0) return false;
|
||||
if (numAmount > 10000) return false; // Max amount check
|
||||
|
||||
// Check decimal places (max 2)
|
||||
const decimalPlaces = (amount.toString().split('.')[1] || '').length;
|
||||
if (decimalPlaces > 2) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse query parameters safely
|
||||
function parseQueryParams(req) {
|
||||
const {
|
||||
wc_order_id,
|
||||
total,
|
||||
currency,
|
||||
customer_email,
|
||||
return_url,
|
||||
cancel_url
|
||||
} = req.query;
|
||||
|
||||
return {
|
||||
wc_order_id: wc_order_id ? parseInt(wc_order_id) : null,
|
||||
total: total ? parseFloat(total) : null,
|
||||
currency: currency || 'USD',
|
||||
customer_email: customer_email || null,
|
||||
return_url: return_url || `${process.env.FRONTEND_URL}/success`,
|
||||
cancel_url: cancel_url || `${process.env.FRONTEND_URL}/cancel`
|
||||
};
|
||||
}
|
||||
|
||||
// Log API request/response
|
||||
function logApiCall(type, endpoint, data, response) {
|
||||
console.log(`${type.toUpperCase()} API Call:`, {
|
||||
endpoint,
|
||||
timestamp: new Date().toISOString(),
|
||||
data: sanitizeLogData(data),
|
||||
response: sanitizeLogData(response),
|
||||
success: response?.success || false
|
||||
});
|
||||
}
|
||||
|
||||
// Sanitize data for logging (remove sensitive info)
|
||||
function sanitizeLogData(data) {
|
||||
if (!data || typeof data !== 'object') return data;
|
||||
|
||||
const sanitized = { ...data };
|
||||
const sensitiveFields = [
|
||||
'password', 'secret', 'token', 'key', 'auth',
|
||||
'email', 'phone', 'address', 'credit_card'
|
||||
];
|
||||
|
||||
sensitiveFields.forEach(field => {
|
||||
if (sanitized[field]) {
|
||||
sanitized[field] = '[REDACTED]';
|
||||
}
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
// Error response helper
|
||||
function createErrorResponse(message, details = null, statusCode = 500) {
|
||||
const response = {
|
||||
success: false,
|
||||
error: message,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
if (details && process.env.NODE_ENV !== 'production') {
|
||||
response.details = details;
|
||||
}
|
||||
|
||||
return { response, statusCode };
|
||||
}
|
||||
|
||||
// Success response helper
|
||||
function createSuccessResponse(data, message = null) {
|
||||
return {
|
||||
success: true,
|
||||
data: data,
|
||||
message: message,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
// Retry mechanism for API calls
|
||||
async function retryOperation(operation, maxRetries = 3, delayMs = 1000) {
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
console.warn(`Operation failed (attempt ${attempt}/${maxRetries}):`, error.message);
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
verifyToken,
|
||||
formatCurrency,
|
||||
isValidEmail,
|
||||
sanitizeOrderData,
|
||||
generateReferenceId,
|
||||
validateAmount,
|
||||
parseQueryParams,
|
||||
logApiCall,
|
||||
sanitizeLogData,
|
||||
createErrorResponse,
|
||||
createSuccessResponse,
|
||||
retryOperation
|
||||
};
|
||||
Reference in New Issue
Block a user