diff --git a/src/controllers/Payment.ts b/src/controllers/Payment.ts index 7cdfb78..92f0cd4 100644 --- a/src/controllers/Payment.ts +++ b/src/controllers/Payment.ts @@ -1,28 +1,54 @@ import express, { Request, Response } from "express"; import Stripe from "stripe"; import dotenv from "dotenv"; -import { findOrderById, findProductById } from "../services/paymentService"; +import { + findOrderById, + findProductById, + findUserById, +} from "../services/paymentService"; +import { sendOrderConfirmation } from "../services/emailService"; dotenv.config(); const stripe = new Stripe(`${process.env.STRIPE_SECRET_KEY}`); + export const checkout = async (req: Request, res: Response) => { try { const orderId = req.params.id; + + // Validate orderId + if (!orderId) { + return res.status(400).json({ message: "Order ID is required" }); + } + const order = await findOrderById(orderId); + if (!order) { return res.status(404).json({ message: "Order not found" }); } + + // Validate order.products + if (!order.products || !Array.isArray(order.products)) { + return res.status(400).json({ message: "Order products are not valid" }); + } + const line_items: any[] = await Promise.all( order.products.map(async (item: any) => { - const productDetails:any = await findProductById(item.productId); - const unit_amount = Math.round(productDetails!.price * 100); + const productDetails: any = await findProductById(item.productId); + + if (!productDetails) { + console.error(`Product with ID ${item.productId} not found`); + throw new Error("Product not found"); + } + + const unit_amount = Math.round(productDetails.price * 100); + return { price_data: { currency: "usd", product_data: { - name: productDetails?.name, - images: [productDetails?.image[0]], + name: productDetails.name, + images: [productDetails.image[0]], }, unit_amount: unit_amount, }, @@ -30,7 +56,6 @@ export const checkout = async (req: Request, res: Response) => { }; }) ); - const session = await stripe.checkout.sessions.create({ line_items, @@ -42,9 +67,9 @@ export const checkout = async (req: Request, res: Response) => { }, }); - res.status(200).json({ url: session.url }); } catch (error: any) { + console.error("Error during checkout process:", error); res.status(500).json({ message: error.message }); } }; @@ -53,27 +78,54 @@ export const webhook = async (req: Request, res: Response) => { const sig: any = req.headers["stripe-signature"]; const webhookSecret: any = process.env.WEBHOOK_SECRET_KEY; let event: any; + try { event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret); } catch (err: any) { - console.log(`⚠️ Webhook signature verification failed.`, err.message); + console.error(`⚠️ Webhook signature verification failed: ${err.message}`); return res.status(400).send(`Webhook Error: ${err.message}`); } switch (event.type) { case "checkout.session.completed": - const session = event.data.object; + const session: any = event.data.object as Stripe.Checkout.Session; + + // Log session details + console.log("Checkout Session Completed Event:"); + console.log("Session ID:", session.id); + console.log("Session Metadata:", session.metadata); + console.log("Session Object:", session); try { + // Fetch line items to get more order details const lineItems = await stripe.checkout.sessions.listLineItems( session.id ); + console.log("Line Items:", lineItems); const orderId = session.metadata.orderId; + + if (!orderId) { + console.error("Order ID not found in session metadata"); + return res.status(400).json({ message: "Order ID not found" }); + } + const order = await findOrderById(orderId); + console.log("Order:", order); + if (order) { order.status = "paid"; await order.save(); + + // Fetch user details and log them + const user = await findUserById(order.userId); + console.log("User:", user); + + if (user) { + await sendOrderConfirmation(user, order); + } else { + console.error("User not found for order:", orderId); + } } else { console.error("Order not found:", orderId); } @@ -83,16 +135,18 @@ export const webhook = async (req: Request, res: Response) => { break; case "payment_intent.succeeded": - const paymentIntent = event.data.object; + const paymentIntent = event.data.object as Stripe.PaymentIntent; console.log("Payment Intent succeeded: ", paymentIntent); break; + case "payment_method.attached": - const paymentMethod = event.data.object; + const paymentMethod = event.data.object as Stripe.PaymentMethod; console.log("Payment Method attached: ", paymentMethod); break; default: console.log(`Unhandled event type ${event.type}`); } + res.json({ received: true }); }; diff --git a/src/controllers/checkout.controller.ts b/src/controllers/checkout.controller.ts index 59099e5..953817b 100644 --- a/src/controllers/checkout.controller.ts +++ b/src/controllers/checkout.controller.ts @@ -2,59 +2,81 @@ import Order from "../database/models/order"; import Cart from "../database/models/cart"; import CartItem from "../database/models/cartitem"; import { Request, Response } from "express"; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; import Product from "../database/models/product"; - +import { + findOrderById, + findProductById, + findUserById, +} from "../services/paymentService"; +import { sendOrderConfirmation } from "../services/emailService"; +import { notifyVendorsOfSale } from "../services/emailService"; export const createOrder = async (req: Request, res: Response) => { - const { userId, deliveryAddress, paymentMethod,client } = req.body; - if(!userId || !deliveryAddress || !paymentMethod) { - return res.status(400).json({ message: 'All fields are required' }) + const { userId, deliveryAddress, paymentMethod, client } = req.body; + if (!userId || !deliveryAddress || !paymentMethod) { + return res.status(400).json({ message: "All fields are required" }); + } + try { + const cart = await Cart.findOne({ where: { userId } }); + if (!cart) { + return res.status(404).json({ message: "Cart not found" }); } - try { - const cart = await Cart.findOne({ where: { userId } }); - if (!cart) { - return res.status(404).json({ message: 'Cart not found' }); - } - const cartItems = await CartItem.findAll({ where: { cartId: cart.cartId } }); - if (cartItems.length === 0) { - return res.status(400).json({ message: "Your cart is empty" }); - } + const cartItems = await CartItem.findAll({ + where: { cartId: cart.cartId }, + }); + if (cartItems.length === 0) { + return res.status(400).json({ message: "Your cart is empty" }); + } - const orderItems = cartItems.map(item => ({ - productId: item.productId, - quantity: item.quantity, - price: item.price, - status:"pending" - })); + const orderItems = cartItems.map((item) => ({ + productId: item.productId, + quantity: item.quantity, + price: item.price, + status: "pending", + })); - const totalAmount = orderItems.reduce((total, item) => total + (item.price * item.quantity), 0); - console.log(orderItems) + const totalAmount = orderItems.reduce( + (total, item) => total + item.price * item.quantity, + 0 + ); + console.log(orderItems); - const order = await Order.create({ - orderId: uuidv4(), - deliveryAddress, - userId, - paymentMethod, - status: 'pending', - products: orderItems, - totalAmount: totalAmount, - client - }); - for (const item of orderItems) { - const product = await Product.findOne({ where: { productId: item.productId } }); - if (product) { - product.quantity -= item.quantity; - if (product.quantity < 0) { - return res.status(400).json({ message: `Not enough stock for product ${product.productId}` }); - } - await product.save(); - } + const order = await Order.create({ + orderId: uuidv4(), + deliveryAddress, + userId, + paymentMethod, + status: "pending", + products: orderItems, + totalAmount: totalAmount, + client, + }); + for (const item of orderItems) { + const product = await Product.findOne({ + where: { productId: item.productId }, + }); + if (product) { + product.quantity -= item.quantity; + if (product.quantity < 0) { + return res.status(400).json({ + message: `Not enough stock for product ${product.productId}`, + }); } - await CartItem.destroy({ where: { cartId: cart.cartId } }); - res.status(201).json({ message: 'Order placed successfully', order }); - } catch (error: any) { - res.status(500).json({ message: error.message }); + await product.save(); + } + } + await CartItem.destroy({ where: { cartId: cart.cartId } }); + const user = await findUserById(order.userId); + if (user) { + await sendOrderConfirmation(user, order); + await notifyVendorsOfSale(order); + } else { + console.error("User not found for order:"); } + res.status(201).json({ message: "Order placed successfully", order }); + } catch (error: any) { + res.status(500).json({ message: error.message }); + } }; diff --git a/src/controllers/orderController.ts b/src/controllers/orderController.ts index f2eea1c..b96b4e8 100644 --- a/src/controllers/orderController.ts +++ b/src/controllers/orderController.ts @@ -73,7 +73,7 @@ export const getAllOrders = async (req: Request, res: Response) => { let orders: any; - orders = await Order.findAll({ where: { userId }}) + orders = await Order.findAll({ where: { userId } }); return res.status(200).json(orders); } catch (error: any) { @@ -105,8 +105,6 @@ export const getOrder = async (req: Request, res: Response) => { } }; - - export const getSellerOrder = async (req: Request, res: Response) => { try { const vendorId = req.params.vendorId; @@ -127,7 +125,7 @@ export const getSellerOrder = async (req: Request, res: Response) => { } } - console.log("products_____:", products) + console.log("products_____:", products); res.status(200).send(products); } catch (error) { diff --git a/src/controllers/orderStatus.controller.ts b/src/controllers/orderStatus.controller.ts index 46a351c..402c27b 100644 --- a/src/controllers/orderStatus.controller.ts +++ b/src/controllers/orderStatus.controller.ts @@ -1,7 +1,29 @@ import { Request, Response } from "express"; import Order from "../database/models/order"; -import { ioServer } from ".."; +import User from "../database/models/user"; import pusher from "../pusher"; +import nodemailer from "nodemailer"; +import dotenv from "dotenv"; +import { ioServer } from ".."; + +dotenv.config(); + +const allowedStatuses = [ + "pending", + "processing", + "shipped", + "delivered", + "on hold", + "cancelled", +]; + +const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: process.env.EMAIL, + pass: process.env.EMAIL_PASS, + }, +}); export const getOrderStatus = async (req: Request, res: Response) => { try { @@ -12,13 +34,11 @@ export const getOrderStatus = async (req: Request, res: Response) => { if (!order) { return res.status(404).send({ message: "Order not found" }); } - res - .status(200) - .json({ - orderId, - status: order.status, - expectedDeliveryDate: order.expectedDeliveryDate, - }); + res.status(200).json({ + orderId, + status: order.status, + expectedDeliveryDate: order.expectedDeliveryDate, + }); } catch (err: any) { res.status(500).json({ message: err.message }); } @@ -30,27 +50,16 @@ export const updateOrderStatus = async (req: Request, res: Response) => { const status = req.body.status; const vendorId = (req as any).vendorId; - const validStatus = [ - "pending", - "processing", - "shipped", - "delivered", - "on hold", - "cancelled", - ]; - - if (!validStatus.includes(status)) { + if (!allowedStatuses.includes(status)) { return res.status(400).json({ message: "Invalid order status" }); } - const order = await Order.findOne({ where: { orderId: orderId } }); + const order = await Order.findOne({ where: { orderId } }); if (!order) { return res.status(404).send({ message: "Order not found" }); } - - order.status = status; if (status === "processing") { @@ -62,14 +71,43 @@ export const updateOrderStatus = async (req: Request, res: Response) => { order.expectedDeliveryDate = expectedDeliveryDate; } - console.log('Updating order to: ', status) + console.log("Updating order to: ", status); await order.save(); - console.log('Order updated successfully: ', order.toJSON()) + console.log("Order updated successfully: ", order.toJSON()); const formattedDate = order.expectedDeliveryDate ? order.expectedDeliveryDate.toLocaleDateString() : null; + // Notify the buyer via email + const buyer = await User.findByPk(order.userId); + if (buyer) { + const emailContent = ` +
Dear ${buyer.name},
+Your order with ID ${orderId} has been updated to "${status}".
+Expected Delivery Date: ${ + formattedDate || "Not set yet" + }
+Thank you for shopping with us!
+Best regards,
+Crafters E-commerce Team
+ `; + + try { + await transporter.sendMail({ + from: `"Crafters E-commerce" <${process.env.EMAIL}>`, + to: buyer.email, + subject: "Order Status Update", + html: emailContent, + }); + console.log("Status update email sent to the buyer."); + } catch (error) { + console.error("Error sending status update email:", error); + } + } + + // Trigger Pusher event and Socket.io event try { await pusher.trigger("order-channel", "order-updated", { orderId, @@ -85,14 +123,13 @@ export const updateOrderStatus = async (req: Request, res: Response) => { status: order.status, expectedDeliveryDate: formattedDate, }); - res - .status(200) - .json({ - orderId, - status: order.status, - expectedDeliveryDate: formattedDate, - }); + + res.status(200).json({ + orderId, + status: order.status, + expectedDeliveryDate: formattedDate, + }); } catch (err: any) { res.status(500).json({ message: err.message }); } -}; \ No newline at end of file +}; diff --git a/src/controllers/product.controller.ts b/src/controllers/product.controller.ts index 7754e64..8da88e3 100644 --- a/src/controllers/product.controller.ts +++ b/src/controllers/product.controller.ts @@ -23,7 +23,6 @@ import { request } from "http"; import { getProductById } from "../services/productService"; import { fetchSimilarProducts } from "../services/productService"; - export const createProduct = async (req: Request, res: Response) => { try { const tokenData = (req as any).token; @@ -47,7 +46,7 @@ export const createProduct = async (req: Request, res: Response) => { category, expiringDate, } = req.body; - console.log(image) + console.log(image); if (!name || !image || !description || !price || !quantity || !category) { return res.status(200).json("All Field are required"); } @@ -56,8 +55,6 @@ export const createProduct = async (req: Request, res: Response) => { return res.status(400).json({ message: "Exactly 4 images are required" }); } - - const data = { name, image: image, @@ -73,7 +70,7 @@ export const createProduct = async (req: Request, res: Response) => { if (!save) { return res.status(500).json({ error: "Failed to save data" }); } - + productLifecycleEmitter.emit(PRODUCT_ADDED, data); return res.status(201).json({ message: "Product Created", data: save }); @@ -106,7 +103,6 @@ export const similarProducts = async (req: Request, res: Response) => { const product = await getProductById(productId); if (!product) { - return res.status(404).json({ error: "Product not found" }); } @@ -116,10 +112,9 @@ export const similarProducts = async (req: Request, res: Response) => { if (similarProducts.length === 0) { return res.status(404).json({ error: "No similar products found" }); } - console.log("hhhhhhhhhh",similarProducts) + console.log("hhhhhhhhhh", similarProducts); return res.status(200).json(similarProducts); - } catch (error: any) { res.status(500).json({ error: error.message }); } @@ -143,7 +138,6 @@ export const readAllProducts = async (req: Request, res: Response) => { } }; - export const searchProduct = async (req: Request, res: Response) => { try { const { name, category } = req.query; @@ -206,7 +200,7 @@ export const deleteProduct = async (req: Request, res: Response) => { const tokenData = (req as any).token; const productId = req.params.id; const { vendorId } = req.body; - console.log(productId) + console.log(productId); const permissionCheck: any = await checkVendorModifyPermission( tokenData, diff --git a/src/database/config/config.js b/src/database/config/config.js index 01ddaa7..90868db 100644 --- a/src/database/config/config.js +++ b/src/database/config/config.js @@ -4,12 +4,6 @@ const config = { development: { url: process.env.DATABASE_DEVELOPMENT_URL, dialect: "postgres", - dialectOptions: { - ssl: { - require: true, - rejectUnauthorized: true, - } - } }, test: { url: process.env.DATABASE_TEST_URL, @@ -18,8 +12,8 @@ const config = { ssl: { require: true, rejectUnauthorized: true, - } - } + }, + }, }, production: { url: process.env.DATABASE_PRODUCTION_URL, @@ -28,8 +22,8 @@ const config = { ssl: { require: true, rejectUnauthorized: true, - } - } + }, + }, }, }; diff --git a/src/database/models/vendor.ts b/src/database/models/vendor.ts index 916f792..7d13e09 100644 --- a/src/database/models/vendor.ts +++ b/src/database/models/vendor.ts @@ -1,27 +1,45 @@ -"use strict"; -import { Model, DataTypes, Sequelize } from "sequelize"; +import { Model, DataTypes, Sequelize, Optional } from "sequelize"; import connectSequelize from "../config/db.config"; -import { ftruncate } from "fs"; +import User from "./user"; // Make sure this path is correct -class Vendor extends Model { +interface VendorAttributes { + vendorId?: string; + userId: string; + storeName: string; + address: any; + TIN: number; + bankAccount: number; + paymentDetails?: any; + status?: string; +} + +interface VendorCreationAttributes + extends OptionalIt's essential to take proactive measures to manage these expiring products effectively. Here are a few suggestions:
Consider offering special promotions or discounts to encourage customers to purchase these items before they expire. This can help minimize losses and increase sales.
- Add Discount + Add DiscountBest regards,
Crafters
Thank you for your purchase. Your payment has been processed successfully.
+ Return to Home +Dear ${user.name},
+Thank you for your purchase! Here are your order details:
+# | +Product Name | +Image | +Price | +
---|
Total Amount Paid: $${order.totalAmount}
+Delivery Address: ${order.deliveryAddress.cell}, ${order.deliveryAddress.district}
+We hope you enjoy your purchase!
+Best regards,
+Crafters E-commerce Team
+ `; + + await transporter.sendMail({ + from: `"Crafters E-commerce" <${process.env.EMAIL}>`, + to: user.email, + subject: "Order Confirmation", + html: emailContent, + }); + + console.log("Order confirmation email sent to customer!"); + } catch (error) { + console.error("Error sending order confirmation email:", error); + } +}; + +// Send notification email to vendors whose products were sold +export const notifyVendorsOfSale = async (order: any) => { + try { + const products = await Product.findAll({ + where: { + productId: { + [Op.in]: order.products.map((item: any) => item.productId), + }, + }, + include: [{ model: Vendor, as: "Vendor" }], + }); + + // Group products by vendor + const vendorProducts: any = {}; + products.forEach((product: any) => { + if (!vendorProducts[product.vendorId]) { + vendorProducts[product.vendorId] = []; + } + vendorProducts[product.vendorId].push(product); + }); + + // Fetch buyer information + const buyer = await User.findOne({ + where: { userId: order.userId }, // Assuming order.userId is the reference to the buyer + }); + + // Send an email to each vendor with details about their sold products + for (const vendorId in vendorProducts) { + const vendor = await Vendor.findOne({ + where: { vendorId }, + include: [{ model: User, as: "user" }], + }); + + if (!vendor) { + console.error(`Vendor with ID ${vendorId} not found.`); + continue; // Use continue to proceed to the next vendor + } + + const soldProducts = vendorProducts[vendorId]; + + const productDetails = soldProducts + .map( + (product: any, index: number) => ` +Dear ${vendor.storeName},
+The following products from your store have been sold:
+# | +Product Name | +Image | +Price | +Quantity Sold | +
---|
Total Amount for these Products: $${soldProducts.reduce( + (total: number, product: any) => + total + + product.price * + order.products.find((p: any) => p.productId === product.productId) + .quantity, + 0 + )}
+Buyer Information:
+Name: ${buyer?.name}
+Email: ${buyer?.email}
+Address: ${order.deliveryAddress.cell}, ${ + order.deliveryAddress.district + }
+We hope you continue enjoying sales through Crafters E-commerce!
+Best regards,
+Crafters E-commerce Team
+ `; + + await transporter.sendMail({ + from: `"Crafters E-commerce" <${process.env.EMAIL}>`, + to: vendor.user?.email ?? "", // Uses optional chaining to avoid accessing 'email' if 'user' is undefined + subject: "Products Sold", + html: vendorEmailContent, + }); + + console.log(`Email notification sent to vendor: ${vendor.storeName}`); + } + } catch (error) { + console.error("Error sending vendor notification emails:", error); + } +}; diff --git a/src/services/paymentService.ts b/src/services/paymentService.ts index 02ea506..e17371b 100644 --- a/src/services/paymentService.ts +++ b/src/services/paymentService.ts @@ -1,12 +1,16 @@ -import Order from "../database/models/order" -import Product from "../database/models/product" +import Order from "../database/models/order"; +import Product from "../database/models/product"; +import User from "../database/models/user"; - -export const findOrderById = async (orderId: any)=>{ - const order = await Order.findByPk(orderId) - return order -} -export const findProductById = async (productId:any)=>{ - const product = await Product.findByPk(productId) - return product -} \ No newline at end of file +export const findOrderById = async (orderId: any) => { + const order = await Order.findByPk(orderId); + return order; +}; +export const findProductById = async (productId: any) => { + const product = await Product.findByPk(productId); + return product; +}; +export const findUserById = async (userId: any) => { + const user = await User.findByPk(userId); + return user; +}; diff --git a/src/services/rolesService.ts b/src/services/rolesService.ts index 10e5761..2434b0f 100644 --- a/src/services/rolesService.ts +++ b/src/services/rolesService.ts @@ -3,11 +3,10 @@ import User from "../database/models/user"; import nodemailer from "nodemailer"; - function toCapital(string: string): string { return string .split(" ") - .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(" "); } @@ -104,14 +103,16 @@ export const approveVendorRequest = async (userId: string) => {We are excited to inform you that your request to become a vendor has been approved!
You can now start listing your products and take advantage of our vendor features.
- Get Started + Get StartedIf you have any questions or need assistance, feel free to contact our support team.
Welcome aboard and happy selling!
Best regards,
Crafter Team