From 8f66fa738cd9ea8ffc0ecfc20ff2be6bcd36f957 Mon Sep 17 00:00:00 2001 From: Gayashan Date: Mon, 28 Apr 2025 14:31:41 +0530 Subject: [PATCH 01/11] modified payment and order --- order-service/{src => }/index.js | 4 +- order-service/package.json | 2 +- .../src/controller/order.controller.js | 78 ++++++++++++++----- .../src/controller/product.controller.js | 38 ++++++++- order-service/src/model/order.model.js | 4 +- order-service/src/model/product.model.js | 5 ++ order-service/src/routes/order.route.js | 6 +- order-service/src/routes/product.route.js | 3 +- payment-service/{src => }/index.js | 10 ++- payment-service/package.json | 2 +- payment-service/src/config/stripe.js | 9 +++ .../src/controller/payment.controllers.js | 29 +++++++ payment-service/src/routes/payment.route.js | 3 +- 13 files changed, 157 insertions(+), 36 deletions(-) rename order-service/{src => }/index.js (78%) rename payment-service/{src => }/index.js (64%) create mode 100644 payment-service/src/config/stripe.js diff --git a/order-service/src/index.js b/order-service/index.js similarity index 78% rename from order-service/src/index.js rename to order-service/index.js index 990b992..ea0de0d 100644 --- a/order-service/src/index.js +++ b/order-service/index.js @@ -1,7 +1,7 @@ import express from 'express'; import dotenv from 'dotenv'; -import connectDB from './service/db.js'; -import orderRoutes from './routes/order.route.js'; +import connectDB from './src/service/db.js'; +import orderRoutes from './src/routes/order.route.js'; dotenv.config(); const app = express(); diff --git a/order-service/package.json b/order-service/package.json index c1d3787..dc2ff37 100644 --- a/order-service/package.json +++ b/order-service/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node src/index.js" + "start": "node index.js" }, "keywords": [], "author": "", diff --git a/order-service/src/controller/order.controller.js b/order-service/src/controller/order.controller.js index 187b7ab..4108244 100644 --- a/order-service/src/controller/order.controller.js +++ b/order-service/src/controller/order.controller.js @@ -6,13 +6,14 @@ dotenv.config(); export const createOrder = async (req, res) => { try { - const { userId, items, paymentMethod, currency, deliveryAddress, phoneNumber } = req.body; + const { userId, items, paymentMethod, totalAmount, currency, deliveryAddress, phoneNumber } = req.body; - if (!userId || !Array.isArray(items) || items.length === 0 || !paymentMethod || !currency || !deliveryAddress) { + if (!userId || !Array.isArray(items) || items.length === 0 || !paymentMethod || !deliveryAddress) { return res.status(400).json({ message: 'Invalid request data' }); } - - let totalAmount = 0; + + let paymentClientSecret = null; + let calculatedTotalAmount = 0; const enrichedItems = []; // Validate and enrich products @@ -23,48 +24,76 @@ export const createOrder = async (req, res) => { } const itemTotal = product.price * item.quantity; - totalAmount += itemTotal; + calculatedTotalAmount += itemTotal; enrichedItems.push({ productId: product._id, quantity: item.quantity, priceAtPurchase: product.price, + restaurantId: product.restaurantId || 'default-restaurant' // Include restaurant ID from product }); } - let clientSecret = null; - - // If payment method is card, call Payment Service + // For card payments, we already processed the payment in the frontend + // so we don't need to call the payment service again if (paymentMethod === 'card') { - const paymentResponse = await axios.post(process.env.PAYMENT_SERVICE_URL, { - amount: totalAmount, - currency, - }); - clientSecret = paymentResponse.data.clientSecret; + // If payment was already processed, we use the totalAmount from the request + // and we don't need to generate a new client secret + } else if (paymentMethod === 'cash') { + // For cash payments, no need to process anything here } - // Create the Order + // Create the Order - use the provided totalAmount for card payments (already processed) + // or use calculated amount for cash payments + const finalAmount = paymentMethod === 'card' ? totalAmount : calculatedTotalAmount; + const order = new Order({ userId, items: enrichedItems, - totalAmount, + totalAmount: finalAmount, + currency: currency || 'usd', // Use provided currency or default to 'usd' paymentMethod, - paymentClientSecret: clientSecret, + paymentClientSecret, deliveryAddress, phoneNumber, + status: paymentMethod === 'card' ? 'paid' : 'pending', // If card payment, it's already paid }); - const savedOrder = await order.save(); + await order.save(); - res.status(201).json(savedOrder); + res.status(201).json({ + message: 'Order created successfully', + order, + paymentClientSecret, + }); } catch (error) { console.error('Order creation failed:', error.message); res.status(500).json({ message: 'Something went wrong on the server' }); } }; +// Add a new endpoint to get orders by restaurant ID +export const getOrdersByRestaurant = async (req, res) => { + try { + const { restaurantId } = req.params; + + if (!restaurantId) { + return res.status(400).json({ message: 'Restaurant ID is required' }); + } + + // Find orders that contain items from the specified restaurant + const orders = await Order.find({ + 'items.restaurantId': restaurantId + }); + + res.status(200).json(orders); + } catch (error) { + console.error('Get restaurant orders failed:', error); + res.status(500).json({ message: 'Something went wrong' }); + } +}; - +// Rest of your controller functions remain the same export const getOrderById = async (req, res) => { try { const order = await Order.findById(req.params.id); @@ -103,7 +132,16 @@ export const deleteOrder = async (req, res) => { export const getAllOrders = async (req, res) => { try { - const orders = await Order.find(); // Fetch all orders from the database + // Support filtering by restaurantId via query param + const { restaurantId } = req.query; + + let query = {}; + if (restaurantId) { + // Find orders with items from this restaurant + query = { 'items.restaurantId': restaurantId }; + } + + const orders = await Order.find(query); res.status(200).json(orders); } catch (error) { console.error('Get all orders failed:', error); diff --git a/order-service/src/controller/product.controller.js b/order-service/src/controller/product.controller.js index c1f3e36..d06f1b1 100644 --- a/order-service/src/controller/product.controller.js +++ b/order-service/src/controller/product.controller.js @@ -5,13 +5,20 @@ dotenv.config(); export const createProduct = async (req, res) => { try { - const { name, description, price, image } = req.body; + const { name, description, price, image, restaurantId } = req.body; if (!name || !price) { return res.status(400).json({ message: 'Name and price are required' }); } - const newProduct = new Product({ name, description, price, image }); + const newProduct = new Product({ + name, + description, + price, + image, + restaurantId: restaurantId || 'default-restaurant' + }); + const savedProduct = await newProduct.save(); res.status(201).json(savedProduct); @@ -33,9 +40,34 @@ export const getProductsById = async (req, res) => { } }; +// Get products by restaurant ID +export const getProductsByRestaurant = async (req, res) => { + try { + const { restaurantId } = req.params; + + if (!restaurantId) { + return res.status(400).json({ message: 'Restaurant ID is required' }); + } + + const products = await Product.find({ restaurantId }); + res.status(200).json(products); + } catch (error) { + console.error('Get restaurant products failed:', error.message); + res.status(500).json({ message: 'Something went wrong' }); + } +}; + export const getAllProducts = async (req, res) => { try { - const products = await Product.find(); + // Support filtering by restaurantId via query param + const { restaurantId } = req.query; + + let query = {}; + if (restaurantId) { + query.restaurantId = restaurantId; + } + + const products = await Product.find(query); res.status(200).json(products); } catch (error) { console.error('Get all products failed:', error.message); diff --git a/order-service/src/model/order.model.js b/order-service/src/model/order.model.js index 9f8879c..60c37c8 100644 --- a/order-service/src/model/order.model.js +++ b/order-service/src/model/order.model.js @@ -3,7 +3,8 @@ import mongoose from 'mongoose'; const itemSchema = new mongoose.Schema({ productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true }, quantity: { type: Number, required: true }, - priceAtPurchase: { type: Number, required: true }, + priceAtPurchase: { type: Number, required: true }, + restaurantId: { type: String, required: true }, // Add restaurant ID to each item }); const orderSchema = new mongoose.Schema( @@ -11,6 +12,7 @@ const orderSchema = new mongoose.Schema( userId: { type: String, required: true }, items: [itemSchema], totalAmount: { type: Number, required: true }, + currency: { type: String, default: 'usd' }, paymentMethod: { type: String, enum: ['card', 'cash'], required: true }, paymentClientSecret: { type: String }, status: { type: String, enum: ['pending', 'paid', 'shipped', 'delivered', 'cancelled'], default: 'pending' }, diff --git a/order-service/src/model/product.model.js b/order-service/src/model/product.model.js index 9007f4e..b3190ff 100644 --- a/order-service/src/model/product.model.js +++ b/order-service/src/model/product.model.js @@ -11,6 +11,11 @@ const productSchema = new mongoose.Schema({ type: Number, required: true, }, + restaurantId: { + type: String, + required: true, + default: 'default-restaurant' // Default value for existing products + }, image: String, }, { timestamps: true }); diff --git a/order-service/src/routes/order.route.js b/order-service/src/routes/order.route.js index a0555e0..fa9c48e 100644 --- a/order-service/src/routes/order.route.js +++ b/order-service/src/routes/order.route.js @@ -4,15 +4,17 @@ import { getOrderById, updateOrder, deleteOrder, - getAllOrders + getAllOrders, + getOrdersByRestaurant } from '../controller/order.controller.js'; const router = express.Router(); router.post('/', createOrder); -router.get('/', getAllOrders); // Fetch all orders router.get('/:id', getOrderById); router.put('/:id', updateOrder); router.delete('/:id', deleteOrder); +router.get('/', getAllOrders); // Fetch all orders +router.get('/restaurant/:restaurantId', getOrdersByRestaurant); // New endpoint for restaurant orders export default router; diff --git a/order-service/src/routes/product.route.js b/order-service/src/routes/product.route.js index ee1e085..74ad52d 100644 --- a/order-service/src/routes/product.route.js +++ b/order-service/src/routes/product.route.js @@ -1,12 +1,13 @@ // src/route/product.route.js import express from 'express'; -import { createProduct, getAllProducts, getProductsById } from '../controller/product.controller.js'; +import { createProduct, getAllProducts, getProductsById, getProductsByRestaurant } from '../controller/product.controller.js'; const router = express.Router(); router.post('/', createProduct); router.get('/:id', getProductsById); router.get('/', getAllProducts); +router.get('/restaurant/:restaurantId', getProductsByRestaurant); // New endpoint for restaurant products export default router; diff --git a/payment-service/src/index.js b/payment-service/index.js similarity index 64% rename from payment-service/src/index.js rename to payment-service/index.js index 1521c8e..83e115e 100644 --- a/payment-service/src/index.js +++ b/payment-service/index.js @@ -1,17 +1,19 @@ import express from 'express'; import dotenv from 'dotenv'; import cors from 'cors'; -import paymentRoutes from './routes/payment.route.js'; +import paymentRoutes from '.'; dotenv.config(); const app = express(); -app.use(cors()); app.use(express.json()); +app.use(cors({ + origin: 'http://localhost:5173', // frontend address +})); -app.use('/api/v1/payment', paymentRoutes); +app.use('/api/v1/payments', paymentRoutes); const PORT = process.env.PORT || 5002; app.listen(PORT, () => { console.log(`Payment service running on port ${PORT}`); -}); +}); \ No newline at end of file diff --git a/payment-service/package.json b/payment-service/package.json index 50b8601..9afa4bf 100644 --- a/payment-service/package.json +++ b/payment-service/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node src/index.js" + "start": "node index.js" }, "keywords": [], "author": "", diff --git a/payment-service/src/config/stripe.js b/payment-service/src/config/stripe.js new file mode 100644 index 0000000..3291643 --- /dev/null +++ b/payment-service/src/config/stripe.js @@ -0,0 +1,9 @@ +import Stripe from 'stripe'; +import dotenv from 'dotenv'; +dotenv.config(); + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: '2023-10-16', // Always set API version +}); + +export default stripe; diff --git a/payment-service/src/controller/payment.controllers.js b/payment-service/src/controller/payment.controllers.js index 73c82fd..4efbc0e 100644 --- a/payment-service/src/controller/payment.controllers.js +++ b/payment-service/src/controller/payment.controllers.js @@ -1,8 +1,10 @@ import dotenv from 'dotenv'; dotenv.config(); import Stripe from 'stripe'; + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); +// CREATE payment intent export const createPaymentIntent = async (req, res) => { try { const { amount, currency } = req.body; @@ -20,3 +22,30 @@ export const createPaymentIntent = async (req, res) => { res.status(500).send({ error: error.message }); } }; + +// NEW: CONFIRM and SAVE payment +export const confirmPayment = async (req, res) => { + try { + const { paymentIntentId } = req.body; + + // Fetch the latest payment intent from Stripe + const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentId); + + if (paymentIntent.status !== 'succeeded') { + return res.status(400).json({ message: 'Payment not successful yet.' }); + } + + await Payment.create({ + paymentIntentId: paymentIntent.id, + amount: paymentIntent.amount, + currency: paymentIntent.currency, + status: paymentIntent.status, + createdAt: new Date(), + }); + + res.status(200).json({ message: 'Payment confirmed and saved successfully.' }); + } catch (error) { + console.error('Payment confirmation failed:', error); + res.status(500).send({ error: error.message }); + } +}; diff --git a/payment-service/src/routes/payment.route.js b/payment-service/src/routes/payment.route.js index d35f476..a5a914e 100644 --- a/payment-service/src/routes/payment.route.js +++ b/payment-service/src/routes/payment.route.js @@ -1,8 +1,9 @@ import express from 'express'; -import { createPaymentIntent } from '../controller/payment.controllers.js'; +import { createPaymentIntent, confirmPayment } from '../controller/payment.controllers.js'; const router = express.Router(); router.post('/create-payment-intent', createPaymentIntent); +router.post('/confirm-payment', confirmPayment); export default router; \ No newline at end of file From da24e9ca1aaeb8915f026dba567aa8fdee74ec33 Mon Sep 17 00:00:00 2001 From: nmdra Date: Mon, 28 Apr 2025 16:06:45 +0530 Subject: [PATCH 02/11] fix: minor changes --- order-service/Dockerfile | 48 ++++++++++++++----- order-service/docker-compose.yml | 4 +- order-service/index.js | 18 ------- order-service/package.json | 3 +- .../src/controller/order.controller.js | 14 +++--- order-service/src/index.js | 32 +++++++++++++ order-service/src/routes/product.route.js | 6 +-- 7 files changed, 82 insertions(+), 43 deletions(-) delete mode 100644 order-service/index.js create mode 100644 order-service/src/index.js diff --git a/order-service/Dockerfile b/order-service/Dockerfile index 1b8d8b6..8fa2b24 100644 --- a/order-service/Dockerfile +++ b/order-service/Dockerfile @@ -1,20 +1,42 @@ -# Use official Node.js image -FROM node:22-alpine +FROM node:22-alpine AS base + +FROM base AS development -# Create app directory WORKDIR /app -# Copy package files -COPY package*.json ./ +COPY package.json yarn.lock ./ + +RUN yarn global add nodemon && yarn install --verbose + +COPY ./src . + +EXPOSE 3001 -# Install dependencies -RUN npm install +CMD ["yarn", "dev"] + +# Production image + +# Builder Stage +FROM base AS builder + +WORKDIR /app + +COPY package.json ./ + +# Install only production dependencies +RUN yarn install --production --verbose && yarn cache clean + +COPY ./src . + +FROM node:22-alpine AS production + +LABEL org.opencontainers.image.title="orderservice" +LABEL org.opencontainers.image.description="CraveDrop order Service" + +WORKDIR /app -# Copy the rest of the code -COPY . . +COPY --from=builder /app . -# Expose the port your app runs on -EXPOSE 5000 +EXPOSE 3000 -# Start the app (ESM-compatible) -CMD ["node", "index.js"] +CMD [ "node","index.js" ] \ No newline at end of file diff --git a/order-service/docker-compose.yml b/order-service/docker-compose.yml index 35ecf93..c45e6c5 100644 --- a/order-service/docker-compose.yml +++ b/order-service/docker-compose.yml @@ -2,7 +2,9 @@ version: '3.8' services: order-service: - build: . + build: + context: . + target: development ports: - "5000:5000" environment: diff --git a/order-service/index.js b/order-service/index.js deleted file mode 100644 index ea0de0d..0000000 --- a/order-service/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import express from 'express'; -import dotenv from 'dotenv'; -import connectDB from './src/service/db.js'; -import orderRoutes from './src/routes/order.route.js'; - -dotenv.config(); -const app = express(); -const PORT = process.env.PORT || 5000; - -app.use(express.json()); - -// Versioned API route -app.use('/api/v1/orders', orderRoutes); - -// Connect MongoDB and start server -connectDB().then(() => { - app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); -}); diff --git a/order-service/package.json b/order-service/package.json index dc2ff37..902d9a7 100644 --- a/order-service/package.json +++ b/order-service/package.json @@ -4,8 +4,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node index.js" + "start": "node src/index.js" }, "keywords": [], "author": "", diff --git a/order-service/src/controller/order.controller.js b/order-service/src/controller/order.controller.js index 4108244..edc011a 100644 --- a/order-service/src/controller/order.controller.js +++ b/order-service/src/controller/order.controller.js @@ -11,7 +11,7 @@ export const createOrder = async (req, res) => { if (!userId || !Array.isArray(items) || items.length === 0 || !paymentMethod || !deliveryAddress) { return res.status(400).json({ message: 'Invalid request data' }); } - + let paymentClientSecret = null; let calculatedTotalAmount = 0; const enrichedItems = []; @@ -61,6 +61,8 @@ export const createOrder = async (req, res) => { await order.save(); + //TODO call notification + res.status(201).json({ message: 'Order created successfully', order, @@ -76,16 +78,16 @@ export const createOrder = async (req, res) => { export const getOrdersByRestaurant = async (req, res) => { try { const { restaurantId } = req.params; - + if (!restaurantId) { return res.status(400).json({ message: 'Restaurant ID is required' }); } - + // Find orders that contain items from the specified restaurant const orders = await Order.find({ 'items.restaurantId': restaurantId }); - + res.status(200).json(orders); } catch (error) { console.error('Get restaurant orders failed:', error); @@ -134,13 +136,13 @@ export const getAllOrders = async (req, res) => { try { // Support filtering by restaurantId via query param const { restaurantId } = req.query; - + let query = {}; if (restaurantId) { // Find orders with items from this restaurant query = { 'items.restaurantId': restaurantId }; } - + const orders = await Order.find(query); res.status(200).json(orders); } catch (error) { diff --git a/order-service/src/index.js b/order-service/src/index.js new file mode 100644 index 0000000..15ad8fe --- /dev/null +++ b/order-service/src/index.js @@ -0,0 +1,32 @@ +import express from 'express'; +import dotenv from 'dotenv'; +import connectDB from './service/db.js'; +import orderRoutes from './routes/order.route.js'; + +dotenv.config(); +const app = express(); +const PORT = process.env.PORT || 5000; + +app.use(express.json()); + +app.get('/api/orders/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); + +// Versioned API route +app.use('/api/orders', orderRoutes); + +// Connect MongoDB and start server +const startServer = async () => { + try { + await connectDB(); + app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + }); + } catch (error) { + console.error('Failed to connect to database:', error.message); + process.exit(1); // Exit the app with failure + } +}; + +startServer(); diff --git a/order-service/src/routes/product.route.js b/order-service/src/routes/product.route.js index 74ad52d..38c5190 100644 --- a/order-service/src/routes/product.route.js +++ b/order-service/src/routes/product.route.js @@ -5,9 +5,9 @@ import { createProduct, getAllProducts, getProductsById, getProductsByRestaurant const router = express.Router(); -router.post('/', createProduct); -router.get('/:id', getProductsById); -router.get('/', getAllProducts); +router.post('/', createProduct); +router.get('/:id', getProductsById); +router.get('/', getAllProducts); router.get('/restaurant/:restaurantId', getProductsByRestaurant); // New endpoint for restaurant products export default router; From 105afb1ac4c4e88ff26219b9a1b1edb6da896c40 Mon Sep 17 00:00:00 2001 From: nmdra Date: Mon, 28 Apr 2025 16:07:37 +0530 Subject: [PATCH 03/11] build: integrate order service --- api-gateway/nginx.conf | 17 ++++++++ docker-bake.hcl | 9 +++- docker-compose.yml | 12 ++++++ .../deployments/order-service-deployment.yaml | 42 +++++++++++++++++++ k8s/kustomization/base/ingress/ingress.yaml | 8 ++++ k8s/kustomization/base/kustomization.yaml | 4 ++ 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 k8s/kustomization/base/deployments/order-service-deployment.yaml diff --git a/api-gateway/nginx.conf b/api-gateway/nginx.conf index 1600420..fb4417b 100644 --- a/api-gateway/nginx.conf +++ b/api-gateway/nginx.conf @@ -12,6 +12,10 @@ http { server notification-service:3000; } + upstream order_service { + server order-service:5000; + } + server { listen 5000; server_name localhost; @@ -42,6 +46,19 @@ http { add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; } + # Order Service + location /api/orders/ { + rewrite ^/api/orders/(.*)$ /orders/$1 break; + proxy_pass http://order_service/; + limit_req zone=api burst=10; + + # CORS headers + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + } + location = /unauthorized { return 401; } diff --git a/docker-bake.hcl b/docker-bake.hcl index 3542bfd..c9a56db 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -28,7 +28,7 @@ target "common" { # Default group builds all services with their specified targets from docker-compose group "default" { - targets = ["frontend", "user-service", "notification-service", "email-service", "sms-service"] + targets = ["frontend", "user-service", "notification-service", "email-service", "sms-service", "order-service"] } # Frontend service @@ -69,3 +69,10 @@ target "sms-service" { target = "production" tags = ["${REGISTRY}/sms-service:${TAG}"] } + +target "order-service" { + inherits = ["common"] + context = "./order-service" + target = "production" + tags = ["${REGISTRY}/order-service:${TAG}"] +} diff --git a/docker-compose.yml b/docker-compose.yml index 519fa0d..6a80ff5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -199,6 +199,18 @@ services: networks: - cravedrop-network + order-service: + image: nmdra/order-service + build: + context: ./order-service + target: production + container_name: order-service + hostname: order-service + ports: + - "3007:5000" + env_file: + - ./order-service/.env + volumes: pg_data: diff --git a/k8s/kustomization/base/deployments/order-service-deployment.yaml b/k8s/kustomization/base/deployments/order-service-deployment.yaml new file mode 100644 index 0000000..0757b49 --- /dev/null +++ b/k8s/kustomization/base/deployments/order-service-deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: order-service + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: order-service + template: + metadata: + labels: + app: order-service + spec: + containers: + - name: order-service + image: nmdra/order-service:latest + imagePullPolicy: Never + ports: + - containerPort: 5000 + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + envFrom: + - configMapRef: + name: order-service-env +--- +apiVersion: v1 +kind: Service +metadata: + name: order-service + namespace: default +spec: + selector: + app: order-service + type: ClusterIP + ports: + - port: 80 + targetPort: 5000 diff --git a/k8s/kustomization/base/ingress/ingress.yaml b/k8s/kustomization/base/ingress/ingress.yaml index 4c5c160..90c08c0 100644 --- a/k8s/kustomization/base/ingress/ingress.yaml +++ b/k8s/kustomization/base/ingress/ingress.yaml @@ -28,6 +28,14 @@ spec: port: number: 80 + - path: /api/orders(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: order-service + port: + number: 80 + - path: /health(/|$)(.*) pathType: ImplementationSpecific backend: diff --git a/k8s/kustomization/base/kustomization.yaml b/k8s/kustomization/base/kustomization.yaml index 6f85af8..d78b4ea 100644 --- a/k8s/kustomization/base/kustomization.yaml +++ b/k8s/kustomization/base/kustomization.yaml @@ -3,6 +3,7 @@ resources: - deployments/notification-service-deployment.yaml - deployments/sms-service-deployment.yaml - deployments/user-service-deployment.yaml + - deployments/order-service-deployment.yaml - deployments/frontend-service-deployment.yaml - rabbitmq/rabbitmq.yaml - ingress/ingress.yaml @@ -20,6 +21,9 @@ configMapGenerator: - name: user-service-env envs: - configs/user-service.env + - name: order-service-env + envs: + - configs/order-service.env generatorOptions: disableNameSuffixHash: true From b746101c31b07b83d12030796a17c04cdea96b98 Mon Sep 17 00:00:00 2001 From: nmdra Date: Mon, 28 Apr 2025 16:19:13 +0530 Subject: [PATCH 04/11] build: minor changes --- .github/workflows/build.yml | 10 +++++----- Makefile | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15cc4ca..7bd667d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,11 +1,11 @@ name: Build and Push Images on: - # push: - # # branches: - # # - main - # tags: - # - 'v*.*.*' + push: + branches: + - main + tags: + - 'v*.*.*' workflow_dispatch: jobs: diff --git a/Makefile b/Makefile index ee57115..21cd2ad 100644 --- a/Makefile +++ b/Makefile @@ -34,4 +34,7 @@ kustomize-view: kubectl kustomize k8s/kustomization/base kustomize-apply: - kubectl apply -k k8s/kustomization/base \ No newline at end of file + kubectl apply -k k8s/kustomization/base + +kustomize-delete: + kubectl delete -k k8s/kustomization/base From 01b7a94bcc11e5f80d4d0e482f8dff1507607f58 Mon Sep 17 00:00:00 2001 From: nmdra Date: Mon, 28 Apr 2025 16:51:38 +0530 Subject: [PATCH 05/11] fix: minor changes --- payment-service/Dockerfile | 41 ++++++++++++++++++++++++++++++------ payment-service/index.js | 19 ----------------- payment-service/package.json | 3 +-- payment-service/src/index.js | 39 ++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 27 deletions(-) delete mode 100644 payment-service/index.js create mode 100644 payment-service/src/index.js diff --git a/payment-service/Dockerfile b/payment-service/Dockerfile index f956e76..d27931d 100644 --- a/payment-service/Dockerfile +++ b/payment-service/Dockerfile @@ -1,13 +1,42 @@ -FROM node:22-alpine +FROM node:22-alpine AS base + +FROM base AS development WORKDIR /app -COPY package*.json ./ +COPY package.json yarn.lock ./ + +RUN yarn global add nodemon && yarn install --verbose + +COPY ./src . + +EXPOSE 3001 -RUN npm install +CMD ["yarn", "dev"] + +# Production image + +# Builder Stage +FROM base AS builder + +WORKDIR /app + +COPY package.json ./ + +# Install only production dependencies +RUN yarn install --production --verbose && yarn cache clean + +COPY ./src . + +FROM node:22-alpine AS production + +LABEL org.opencontainers.image.title="paymentservice" +LABEL org.opencontainers.image.description="CraveDrop payment Service" + +WORKDIR /app -COPY . . +COPY --from=builder /app . -EXPOSE 5002 +EXPOSE 3000 -CMD ["npm", "start"] \ No newline at end of file +CMD [ "node","index.js" ] \ No newline at end of file diff --git a/payment-service/index.js b/payment-service/index.js deleted file mode 100644 index 83e115e..0000000 --- a/payment-service/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import express from 'express'; -import dotenv from 'dotenv'; -import cors from 'cors'; -import paymentRoutes from '.'; - -dotenv.config(); - -const app = express(); -app.use(express.json()); -app.use(cors({ - origin: 'http://localhost:5173', // frontend address -})); - -app.use('/api/v1/payments', paymentRoutes); - -const PORT = process.env.PORT || 5002; -app.listen(PORT, () => { - console.log(`Payment service running on port ${PORT}`); -}); \ No newline at end of file diff --git a/payment-service/package.json b/payment-service/package.json index 9afa4bf..4033fa6 100644 --- a/payment-service/package.json +++ b/payment-service/package.json @@ -4,8 +4,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node index.js" + "start": "node src/index.js" }, "keywords": [], "author": "", diff --git a/payment-service/src/index.js b/payment-service/src/index.js new file mode 100644 index 0000000..43b03e3 --- /dev/null +++ b/payment-service/src/index.js @@ -0,0 +1,39 @@ +import express from 'express'; +import dotenv from 'dotenv'; +import cors from 'cors'; +import paymentRoutes from './routes/payment.route.js'; + +dotenv.config(); + +const app = express(); +app.use(express.json()); + +// CORS configuration with frontend URL from environment +const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:5173'; + +app.use(cors({ + origin: FRONTEND_URL, +})); + +app.get('/api/payments/health', (req, res) => { + res.status(200).json({ status: 'ok' }); +}); + +// Payment routes +app.use('/api/payments', paymentRoutes); + +// Start server with error handling +const PORT = process.env.PORT || 5002; + +const startServer = async () => { + try { + app.listen(PORT, () => { + console.log(`Payment service running on port ${PORT}`); + }); + } catch (error) { + console.error('Failed to start server:', error.message); + process.exit(1); + } +}; + +startServer(); From 06a9a1475d4027bbd96a567d30d1e6dd7a8ff41b Mon Sep 17 00:00:00 2001 From: nmdra Date: Mon, 28 Apr 2025 16:52:18 +0530 Subject: [PATCH 06/11] build: integrate payment service --- api-gateway/nginx.conf | 17 ++++++++ docker-bake.hcl | 11 ++++- docker-compose.yml | 12 ++++++ .../payment-service-deployment.yaml | 42 +++++++++++++++++++ k8s/kustomization/base/ingress/ingress.yaml | 8 ++++ k8s/kustomization/base/kustomization.yaml | 4 ++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 k8s/kustomization/base/deployments/payment-service-deployment.yaml diff --git a/api-gateway/nginx.conf b/api-gateway/nginx.conf index fb4417b..70e1a60 100644 --- a/api-gateway/nginx.conf +++ b/api-gateway/nginx.conf @@ -16,6 +16,10 @@ http { server order-service:5000; } + upstream payment_service { + server payment-service:5000; + } + server { listen 5000; server_name localhost; @@ -59,6 +63,19 @@ http { add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; } + # Payment Service + location /api/payments/ { + rewrite ^/api/payments/(.*)$ /orders/$1 break; + proxy_pass http://payment__service/; + limit_req zone=api burst=10; + + # CORS headers + add_header Access-Control-Allow-Origin "*" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Headers "Authorization, Content-Type" always; + add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always; + } + location = /unauthorized { return 401; } diff --git a/docker-bake.hcl b/docker-bake.hcl index c9a56db..7f702ba 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -28,7 +28,7 @@ target "common" { # Default group builds all services with their specified targets from docker-compose group "default" { - targets = ["frontend", "user-service", "notification-service", "email-service", "sms-service", "order-service"] + targets = ["frontend", "user-service", "notification-service", "email-service", "sms-service", "order-service", "payment-service"] } # Frontend service @@ -70,9 +70,18 @@ target "sms-service" { tags = ["${REGISTRY}/sms-service:${TAG}"] } +# Order Service target "order-service" { inherits = ["common"] context = "./order-service" target = "production" tags = ["${REGISTRY}/order-service:${TAG}"] } + +# Payment Service +target "payment-service" { + inherits = ["common"] + context = "./payment-service" + target = "production" + tags = ["${REGISTRY}/payment-service:${TAG}"] +} diff --git a/docker-compose.yml b/docker-compose.yml index 6a80ff5..48a3c90 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -211,6 +211,18 @@ services: env_file: - ./order-service/.env + payment-service: + image: nmdra/payment-service + build: + context: ./payment-service + target: production + container_name: payment-service + hostname: payment-service + ports: + - "3008:5000" + env_file: + - ./payment-service/.env + volumes: pg_data: diff --git a/k8s/kustomization/base/deployments/payment-service-deployment.yaml b/k8s/kustomization/base/deployments/payment-service-deployment.yaml new file mode 100644 index 0000000..bbdeea9 --- /dev/null +++ b/k8s/kustomization/base/deployments/payment-service-deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: payment-service + namespace: default +spec: + replicas: 2 + selector: + matchLabels: + app: payment-service + template: + metadata: + labels: + app: payment-service + spec: + containers: + - name: payment-service + image: nmdra/payment-service:latest + imagePullPolicy: Never + ports: + - containerPort: 5000 + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + envFrom: + - configMapRef: + name: payment-service-env +--- +apiVersion: v1 +kind: Service +metadata: + name: payment-service + namespace: default +spec: + selector: + app: payment-service + type: ClusterIP + ports: + - port: 80 + targetPort: 5000 diff --git a/k8s/kustomization/base/ingress/ingress.yaml b/k8s/kustomization/base/ingress/ingress.yaml index 90c08c0..20aee3b 100644 --- a/k8s/kustomization/base/ingress/ingress.yaml +++ b/k8s/kustomization/base/ingress/ingress.yaml @@ -36,6 +36,14 @@ spec: port: number: 80 + - path: /api/payments(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: payment-service + port: + number: 80 + - path: /health(/|$)(.*) pathType: ImplementationSpecific backend: diff --git a/k8s/kustomization/base/kustomization.yaml b/k8s/kustomization/base/kustomization.yaml index d78b4ea..abb8c1c 100644 --- a/k8s/kustomization/base/kustomization.yaml +++ b/k8s/kustomization/base/kustomization.yaml @@ -4,6 +4,7 @@ resources: - deployments/sms-service-deployment.yaml - deployments/user-service-deployment.yaml - deployments/order-service-deployment.yaml + - deployments/payment-service-deployment.yaml - deployments/frontend-service-deployment.yaml - rabbitmq/rabbitmq.yaml - ingress/ingress.yaml @@ -24,6 +25,9 @@ configMapGenerator: - name: order-service-env envs: - configs/order-service.env + - name: payment-service-env + envs: + - configs/payment-service.env generatorOptions: disableNameSuffixHash: true From 1e91f869f068d01e632f5da10e743166ea9f0a12 Mon Sep 17 00:00:00 2001 From: nmdra Date: Mon, 28 Apr 2025 17:22:53 +0530 Subject: [PATCH 07/11] doc: add deployment guide --- k8s/kustomization/README.md | 142 ++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 k8s/kustomization/README.md diff --git a/k8s/kustomization/README.md b/k8s/kustomization/README.md new file mode 100644 index 0000000..0610db9 --- /dev/null +++ b/k8s/kustomization/README.md @@ -0,0 +1,142 @@ +# Deployment Guide + +- [Deployment Guide](#deployment-guide) + - [0. Directory Structute](#0-directory-structute) + - [1. Environment Configuration](#1-environment-configuration) + - [2. ConfigMap Generation](#2-configmap-generation) + - [3. Deploy the Application](#3-deploy-the-application) + - [4. PostgreSQL Installation (via Helm)](#4-postgresql-installation-via-helm) + - [Step 1: Add Bitnami Repository](#step-1-add-bitnami-repository) + - [Step 2: Install PostgreSQL](#step-2-install-postgresql) + - [Step 3: Verify Installation](#step-3-verify-installation) + - [5. Notes](#5-notes) + +## 0. Directory Structute + +```plaintext +. +├── base +│ ├── configs +│ │ ├── email-service.env +│ │ ├── notification-service.env +│ │ ├── order-service.env +│ │ ├── payment-service.env +│ │ ├── sms-service.env +│ │ └── user-service.env +│ ├── deployments +│ │ ├── email-service-deployment.yaml +│ │ ├── frontend-service-deployment.yaml +│ │ ├── notification-service-deployment.yaml +│ │ ├── order-service-deployment.yaml +│ │ ├── payment-service-deployment.yaml +│ │ ├── sms-service-deployment.yaml +│ │ └── user-service-deployment.yaml +│ ├── ingress +│ │ └── ingress.yaml +│ ├── kustomization.yaml +│ └── rabbitmq +│ └── rabbitmq.yaml +└── README.md +``` + +## 1. Environment Configuration + +Before deploying, copy the environment files to the `configs/` directory and rename them properly if needed. + +```bash +# Example +cp email-service/.env base/configs/email-service.env +cp notification-service/.env base/configs/notification-service.env +cp order-service/.env base/configs/order-service.env +cp payment-service/.env base/configs/payment-service.env +cp sms-service/.env base/configs/sms-service.env +cp user-service/.env base/configs/user-service.env +``` + +Make sure each service has the correct `.env` file placed inside `base/configs/`. + +--- + +## 2. ConfigMap Generation + +Kustomize automatically **generates ConfigMaps** from the files under `base/configs/` based on the `kustomization.yaml` configuration. +You **do not need to manually create ConfigMaps** — they are created during `kubectl apply -k`. + +> Learn more: [Kustomize ConfigMap Generation](https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/configmapgenerator/) + +Example snippet from `kustomization.yaml`: + +```yaml +configMapGenerator: + - name: user-service-config + envs: + - configs/user-service.env + - name: email-service-config + envs: + - configs/email-service.env + # (other services...) +``` +--- + +## 3. Deploy the Application + +Apply the full base configuration to your cluster: + +```bash +kubectl apply -k k8s/base +``` + +This will deploy: +- Microservices (User, Order, Payment, Notification, Email, SMS) +- Frontend +- RabbitMQ +- Ingress + +--- + +## 4. PostgreSQL Installation (via Helm) + +Install **PostgreSQL** using the Bitnami Helm chart: + +### Step 1: Add Bitnami Repository + +```bash +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +``` + +### Step 2: Install PostgreSQL + +```bash +helm install postgres-release bitnami/postgresql \ + --set auth.username=youruser \ + --set auth.password=yourpassword \ + --set auth.database=yourdatabase +``` + +🔵 **Replace** `youruser`, `yourpassword`, and `yourdatabase` with your actual credentials. + +You can also customize values further using a `values.yaml` file if needed. + +### Step 3: Verify Installation + +```bash +kubectl get pods +kubectl get svc +``` + +Make sure the PostgreSQL pod and service are running! + +> Full chart documentation: [Bitnami PostgreSQL Helm Chart](https://artifacthub.io/packages/helm/bitnami/postgresql) + +--- + +## 5. Notes + +- Make sure RabbitMQ is running first if services depend on it. +- If needed, update `ingress/ingress.yaml` according to your domain or host settings. +- Make sure your Kubernetes cluster has an Ingress Controller (like NGINX) installed. + + + + From 5c65f1a5c4ea674d3f3886f7b691c6d3e347817d Mon Sep 17 00:00:00 2001 From: Gayashan Date: Mon, 28 Apr 2025 20:29:33 +0530 Subject: [PATCH 08/11] add order part in frontend --- frontend/package.json | 5 +- frontend/src/App.jsx | 22 +- frontend/src/Components/Home/ShopList.jsx | 2 +- frontend/src/Components/order/ProductCard.jsx | 30 +++ frontend/src/Context/CartContext.jsx | 20 ++ frontend/src/Pages/order/CartPage.jsx | 185 +++++++++++++++ frontend/src/Pages/order/Checkoutpage.jsx | 175 ++++++++++++++ frontend/src/Pages/order/HomePage.jsx | 163 ++++++++++++++ frontend/src/Pages/order/PaymentPage.jsx | 147 ++++++++++++ frontend/src/Pages/order/ProductDetails.jsx | 213 ++++++++++++++++++ frontend/src/Pages/order/SuccessPage.jsx | 12 + frontend/src/api/orderApi.js | 5 + frontend/src/api/productApi.js | 6 + frontend/src/services/Stripe.jsx | 11 + frontend/src/services/localStorageUtils.jsx | 0 frontend/yarn.lock | 61 ++++- 16 files changed, 1048 insertions(+), 9 deletions(-) create mode 100644 frontend/src/Components/order/ProductCard.jsx create mode 100644 frontend/src/Context/CartContext.jsx create mode 100644 frontend/src/Pages/order/CartPage.jsx create mode 100644 frontend/src/Pages/order/Checkoutpage.jsx create mode 100644 frontend/src/Pages/order/HomePage.jsx create mode 100644 frontend/src/Pages/order/PaymentPage.jsx create mode 100644 frontend/src/Pages/order/ProductDetails.jsx create mode 100644 frontend/src/Pages/order/SuccessPage.jsx create mode 100644 frontend/src/api/orderApi.js create mode 100644 frontend/src/api/productApi.js create mode 100644 frontend/src/services/Stripe.jsx create mode 100644 frontend/src/services/localStorageUtils.jsx diff --git a/frontend/package.json b/frontend/package.json index 585bba4..022ed23 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,12 @@ "preview": "vite preview" }, "dependencies": { + "@stripe/react-stripe-js": "^3.6.0", + "@stripe/stripe-js": "^7.2.0", "@tailwindcss/vite": "^4.1.4", - "axios": "^1.8.4", + "axios": "^1.9.0", "axios-mock-adapter": "^2.1.0", + "cors": "^2.8.5", "react": "^19.0.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.2", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 543c1c0..2e3dbdf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -20,6 +20,15 @@ import Notifications from './Pages/Customer/Notifications' import SidebarLayout from './Layouts/SidebarLayout' import OrderSummary from './Pages/Customer/OrderSummary' +//order part +import { CartProvider } from './Context/CartContext' +import Home from './Pages/order/HomePage' +import ProductDetails from './Pages/order/ProductDetails' +import Cart from './Pages/order/CartPage' +import Checkout from './Pages/order/CheckoutPage' +import Payment from './Pages/order/PaymentPage' +import SuccessPage from './Pages/order/SuccessPage' + const router = createBrowserRouter( createRoutesFromElements( <> @@ -37,6 +46,15 @@ const router = createBrowserRouter( } /> + {/* order part */} + } /> + } /> + } /> + } /> + } /> + } /> + + {/* Catch-all for 404 */} } /> @@ -46,10 +64,10 @@ const router = createBrowserRouter( const App = () => { return ( - <> + - + ) } diff --git a/frontend/src/Components/Home/ShopList.jsx b/frontend/src/Components/Home/ShopList.jsx index 1088c1a..458cfb7 100644 --- a/frontend/src/Components/Home/ShopList.jsx +++ b/frontend/src/Components/Home/ShopList.jsx @@ -97,7 +97,7 @@ const FoodList = () => {
diff --git a/frontend/src/Components/order/ProductCard.jsx b/frontend/src/Components/order/ProductCard.jsx new file mode 100644 index 0000000..2b32f5b --- /dev/null +++ b/frontend/src/Components/order/ProductCard.jsx @@ -0,0 +1,30 @@ +import React from 'react'; + +const ProductCard = ({ product }) => { + return ( +
+
+ {product.name} +
+ New +
+
+
+

{product.name}

+

{product.description}

+
+ ${product.price.toFixed(2)} + +
+
+
+ ); +}; + +export default ProductCard; \ No newline at end of file diff --git a/frontend/src/Context/CartContext.jsx b/frontend/src/Context/CartContext.jsx new file mode 100644 index 0000000..2d823aa --- /dev/null +++ b/frontend/src/Context/CartContext.jsx @@ -0,0 +1,20 @@ +import { createContext, useState, useEffect } from 'react'; + +export const CartContext = createContext(); + +export const CartProvider = ({ children }) => { + const [cartItems, setCartItems] = useState(() => { + const storedCart = localStorage.getItem('cartItems'); + return storedCart ? JSON.parse(storedCart) : []; + }); + + useEffect(() => { + localStorage.setItem('cartItems', JSON.stringify(cartItems)); + }, [cartItems]); + + return ( + + {children} + + ); +}; diff --git a/frontend/src/Pages/order/CartPage.jsx b/frontend/src/Pages/order/CartPage.jsx new file mode 100644 index 0000000..64364ed --- /dev/null +++ b/frontend/src/Pages/order/CartPage.jsx @@ -0,0 +1,185 @@ +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +function CartPage() { + const [cartItems, setCartItems] = useState([]); + const [selectedItems, setSelectedItems] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + const cart = JSON.parse(localStorage.getItem('cart')) || []; + setCartItems(cart); + }, []); + + const handleCheckboxChange = (item) => { + const isSelected = selectedItems.some((i) => i._id === item._id); + if (isSelected) { + setSelectedItems(selectedItems.filter((i) => i._id !== item._id)); + } else { + setSelectedItems([...selectedItems, item]); + } + }; + + // Add function to update quantity + const updateQuantity = (itemId, newQuantity) => { + // Don't allow quantities less than 1 + if (newQuantity < 1) return; + + // Update cart items state + const updatedCart = cartItems.map(item => { + if (item._id === itemId) { + return { ...item, quantity: newQuantity }; + } + return item; + }); + + setCartItems(updatedCart); + + // Update selected items if this item is selected + if (selectedItems.some(item => item._id === itemId)) { + setSelectedItems(prevSelected => + prevSelected.map(item => { + if (item._id === itemId) { + return { ...item, quantity: newQuantity }; + } + return item; + }) + ); + } + + // Update localStorage + localStorage.setItem('cart', JSON.stringify(updatedCart)); + }; + + // Add function to remove item from cart + const removeFromCart = (itemId) => { + const updatedCart = cartItems.filter(item => item._id !== itemId); + setCartItems(updatedCart); + setSelectedItems(selectedItems.filter(item => item._id !== itemId)); + localStorage.setItem('cart', JSON.stringify(updatedCart)); + }; + + const totalAmount = selectedItems.reduce((acc, item) => acc + item.price * item.quantity, 0); + + const handleProceedToCheckout = () => { + if (selectedItems.length === 0) { + alert('Please select at least one item to checkout.'); + return; + } + localStorage.setItem('checkoutItems', JSON.stringify(selectedItems)); + navigate('/checkout'); + }; + + return ( +
+
+

Your Cart

+ + {cartItems.length === 0 ? ( +
+

Your cart is empty.

+ +
+ ) : ( + <> +
+ {cartItems.map((item, index) => { + const isSelected = selectedItems.some((i) => i._id === item._id); + return ( +
+ handleCheckboxChange(item)} + className="w-5 h-5 mr-4 accent-blue-500" + /> +
+

{item.name}

+

Price: Rs {item.price}

+ + {/* Quantity controls */} +
+ Quantity: + + updateQuantity(item._id, parseInt(e.target.value) || 1)} + className="w-12 h-8 text-center border-y border-gray-200 focus:outline-none" + /> + +
+ +

Subtotal: Rs {item.price * item.quantity}

+
+ + {/* Remove button */} + +
+ ); + })} +
+ +
+

+ Total for selected: Rs {totalAmount} +

+ +
+ + + +
+
+ + )} +
+
+ ); +} + +export default CartPage; diff --git a/frontend/src/Pages/order/Checkoutpage.jsx b/frontend/src/Pages/order/Checkoutpage.jsx new file mode 100644 index 0000000..ef88827 --- /dev/null +++ b/frontend/src/Pages/order/Checkoutpage.jsx @@ -0,0 +1,175 @@ +import { useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import axios from 'axios'; + +function CheckoutPage() { + const [checkoutItems, setCheckoutItems] = useState([]); + const [paymentMethod, setPaymentMethod] = useState('cash'); + const [address, setAddress] = useState(''); + const [phone, setPhone] = useState(''); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + useEffect(() => { + const items = JSON.parse(localStorage.getItem('checkoutItems')) || []; + setCheckoutItems(items); + }, []); + + const totalAmount = checkoutItems.reduce((acc, item) => acc + item.price * item.quantity, 0); + + const handlePlaceOrder = async () => { + if (!address.trim() || !phone.trim()) { + alert('Please fill address and phone number.'); + return; + } + + if (checkoutItems.length === 0) { + alert('No items selected for checkout.'); + return; + } + + setLoading(true); + + try { + // Format items the same way as in PaymentPage + const orderItems = checkoutItems.map((item) => ({ + productId: item._id, + quantity: item.quantity, + })); + + if (paymentMethod === 'cash') { + // Cash on delivery order creation + const response = await axios.post('http://localhost:5000/api/orders', { + userId: 'user-123', + items: orderItems.map(item => ({ + ...item, + // Retrieve restaurantId from localStorage or use default + restaurantId: localStorage.getItem(`restaurant_${item.productId}`) || 'default-restaurant' + })), + paymentMethod, + currency: 'usd', + deliveryAddress: address, + phoneNumber: phone, + // Note: When using cash payment, backend calculates totalAmount + }); + + console.log('Order created:', response.data); + alert('Order placed successfully!'); + localStorage.removeItem('cart'); + localStorage.removeItem('checkoutItems'); + navigate('/'); + } else if (paymentMethod === 'card') { + // Create payment intent first + const paymentResponse = await axios.post('http://localhost:5002/api/payments/create-payment-intent', { + amount: Math.round(totalAmount * 100), // Convert to cents and ensure it's an integer + currency: 'usd', + }); + + // Store necessary data for payment page + localStorage.setItem('paymentItems', JSON.stringify(orderItems)); + localStorage.setItem('paymentAddress', address); + localStorage.setItem('paymentPhone', phone); + localStorage.setItem('paymentMethod', paymentMethod); + localStorage.setItem('paymentClientSecret', paymentResponse.data.clientSecret); + + navigate('/payment'); + } + } catch (error) { + console.error('Error placing order:', error); + + if (error.response) { + console.error('Response data:', error.response.data); + console.error('Response status:', error.response.status); + alert(`Error: ${error.response.data.message || 'Something went wrong. Please try again.'}`); + } else { + alert('Something went wrong. Please try again.'); + } + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Checkout

+ + {checkoutItems.length === 0 ? ( +
+

No items to checkout.

+
+ ) : ( + <> +
+ {checkoutItems.map((item, index) => ( +
+

{item.name}

+

Price: Rs {item.price}

+

Quantity: {item.quantity}

+

+ Subtotal: Rs {item.price * item.quantity} +

+
+ ))} +
+ +
+

Total Amount: Rs {totalAmount}

+ +
+
+ +