payment for rental from renter stripe integration

This commit is contained in:
jackiettran
2025-08-27 19:46:27 -04:00
parent 601e11b7e8
commit 38346bec27
13 changed files with 1090 additions and 421 deletions

View File

@@ -1,7 +1,7 @@
const express = require('express');
const { Op } = require('sequelize');
const { Rental, Item, User } = require('../models'); // Import from models/index.js to get models with associations
const { authenticateToken } = require('../middleware/auth');
const express = require("express");
const { Op } = require("sequelize");
const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations
const { authenticateToken } = require("../middleware/auth");
const router = express.Router();
// Helper function to check and update review visibility
@@ -31,7 +31,8 @@ const checkAndUpdateReviewVisibility = async (rental) => {
// Check renter review visibility (10-minute rule)
if (rental.renterReviewSubmittedAt && !rental.renterReviewVisible) {
const timeSinceSubmission = now - new Date(rental.renterReviewSubmittedAt);
const timeSinceSubmission =
now - new Date(rental.renterReviewSubmittedAt);
if (timeSinceSubmission >= tenMinutesInMs) {
updates.renterReviewVisible = true;
needsUpdate = true;
@@ -46,79 +47,98 @@ const checkAndUpdateReviewVisibility = async (rental) => {
return rental;
};
router.get('/my-rentals', authenticateToken, async (req, res) => {
router.get("/my-rentals", authenticateToken, async (req, res) => {
try {
const rentals = await Rental.findAll({
where: { renterId: req.user.id },
// Remove explicit attributes to let Sequelize handle missing columns gracefully
include: [
{ model: Item, as: 'item' },
{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] }
{ model: Item, as: "item" },
{
model: User,
as: "owner",
attributes: ["id", "username", "firstName", "lastName"],
},
],
order: [['createdAt', 'DESC']]
order: [["createdAt", "DESC"]],
});
console.log('My-rentals data:', rentals.length > 0 ? rentals[0].toJSON() : 'No rentals');
res.json(rentals);
} catch (error) {
console.error('Error in my-rentals route:', error);
console.error("Error in my-rentals route:", error);
res.status(500).json({ error: error.message });
}
});
router.get('/my-listings', authenticateToken, async (req, res) => {
router.get("/my-listings", authenticateToken, async (req, res) => {
try {
const rentals = await Rental.findAll({
where: { ownerId: req.user.id },
// Remove explicit attributes to let Sequelize handle missing columns gracefully
include: [
{ model: Item, as: 'item' },
{ model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] }
{ model: Item, as: "item" },
{
model: User,
as: "renter",
attributes: ["id", "username", "firstName", "lastName"],
},
],
order: [['createdAt', 'DESC']]
order: [["createdAt", "DESC"]],
});
console.log('My-listings rentals:', rentals.length > 0 ? rentals[0].toJSON() : 'No rentals');
res.json(rentals);
} catch (error) {
console.error('Error in my-listings route:', error);
console.error("Error in my-listings route:", error);
res.status(500).json({ error: error.message });
}
});
router.post('/', authenticateToken, async (req, res) => {
router.post("/", authenticateToken, async (req, res) => {
try {
const { itemId, startDate, endDate, startTime, endTime, deliveryMethod, deliveryAddress, notes } = req.body;
const {
itemId,
startDate,
endDate,
startTime,
endTime,
deliveryMethod,
deliveryAddress,
notes,
} = req.body;
const item = await Item.findByPk(itemId);
if (!item) {
return res.status(404).json({ error: 'Item not found' });
return res.status(404).json({ error: "Item not found" });
}
if (!item.availability) {
return res.status(400).json({ error: 'Item is not available' });
return res.status(400).json({ error: "Item is not available" });
}
const overlappingRental = await Rental.findOne({
where: {
itemId,
status: { [Op.in]: ['confirmed', 'active'] },
status: { [Op.in]: ["confirmed", "active"] },
[Op.or]: [
{
startDate: { [Op.between]: [startDate, endDate] }
startDate: { [Op.between]: [startDate, endDate] },
},
{
endDate: { [Op.between]: [startDate, endDate] }
}
]
}
endDate: { [Op.between]: [startDate, endDate] },
},
],
},
});
if (overlappingRental) {
return res.status(400).json({ error: 'Item is already booked for these dates' });
return res
.status(400)
.json({ error: "Item is already booked for these dates" });
}
const rentalDays = Math.ceil((new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24));
const rentalDays = Math.ceil(
(new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)
);
const totalAmount = rentalDays * (item.pricePerDay || 0);
const rental = await Rental.create({
@@ -132,15 +152,23 @@ router.post('/', authenticateToken, async (req, res) => {
totalAmount,
deliveryMethod,
deliveryAddress,
notes
notes,
});
const rentalWithDetails = await Rental.findByPk(rental.id, {
include: [
{ model: Item, as: 'item' },
{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] },
{ model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] }
]
{ model: Item, as: "item" },
{
model: User,
as: "owner",
attributes: ["id", "username", "firstName", "lastName"],
},
{
model: User,
as: "renter",
attributes: ["id", "username", "firstName", "lastName"],
},
],
});
res.status(201).json(rentalWithDetails);
@@ -149,27 +177,35 @@ router.post('/', authenticateToken, async (req, res) => {
}
});
router.put('/:id/status', authenticateToken, async (req, res) => {
router.put("/:id/status", authenticateToken, async (req, res) => {
try {
const { status } = req.body;
const rental = await Rental.findByPk(req.params.id);
if (!rental) {
return res.status(404).json({ error: 'Rental not found' });
return res.status(404).json({ error: "Rental not found" });
}
if (rental.ownerId !== req.user.id && rental.renterId !== req.user.id) {
return res.status(403).json({ error: 'Unauthorized' });
return res.status(403).json({ error: "Unauthorized" });
}
await rental.update({ status });
const updatedRental = await Rental.findByPk(rental.id, {
include: [
{ model: Item, as: 'item' },
{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] },
{ model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] }
]
{ model: Item, as: "item" },
{
model: User,
as: "owner",
attributes: ["id", "username", "firstName", "lastName"],
},
{
model: User,
as: "renter",
attributes: ["id", "username", "firstName", "lastName"],
},
],
});
res.json(updatedRental);
@@ -179,33 +215,35 @@ router.put('/:id/status', authenticateToken, async (req, res) => {
});
// Owner reviews renter
router.post('/:id/review-renter', authenticateToken, async (req, res) => {
router.post("/:id/review-renter", authenticateToken, async (req, res) => {
try {
const { rating, review, privateMessage } = req.body;
const rental = await Rental.findByPk(req.params.id);
if (!rental) {
return res.status(404).json({ error: 'Rental not found' });
return res.status(404).json({ error: "Rental not found" });
}
if (rental.ownerId !== req.user.id) {
return res.status(403).json({ error: 'Only owners can review renters' });
return res.status(403).json({ error: "Only owners can review renters" });
}
if (rental.status !== 'completed') {
return res.status(400).json({ error: 'Can only review completed rentals' });
if (rental.status !== "completed") {
return res
.status(400)
.json({ error: "Can only review completed rentals" });
}
if (rental.renterReviewSubmittedAt) {
return res.status(400).json({ error: 'Renter review already submitted' });
return res.status(400).json({ error: "Renter review already submitted" });
}
// Submit the review and private message
await rental.update({
renterRating: rating,
await rental.update({
renterRating: rating,
renterReview: review,
renterReviewSubmittedAt: new Date(),
renterPrivateMessage: privateMessage
renterPrivateMessage: privateMessage,
});
// Check and update visibility
@@ -213,7 +251,7 @@ router.post('/:id/review-renter', authenticateToken, async (req, res) => {
res.json({
success: true,
reviewVisible: updatedRental.renterReviewVisible
reviewVisible: updatedRental.renterReviewVisible,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -221,33 +259,35 @@ router.post('/:id/review-renter', authenticateToken, async (req, res) => {
});
// Renter reviews item
router.post('/:id/review-item', authenticateToken, async (req, res) => {
router.post("/:id/review-item", authenticateToken, async (req, res) => {
try {
const { rating, review, privateMessage } = req.body;
const rental = await Rental.findByPk(req.params.id);
if (!rental) {
return res.status(404).json({ error: 'Rental not found' });
return res.status(404).json({ error: "Rental not found" });
}
if (rental.renterId !== req.user.id) {
return res.status(403).json({ error: 'Only renters can review items' });
return res.status(403).json({ error: "Only renters can review items" });
}
if (rental.status !== 'completed') {
return res.status(400).json({ error: 'Can only review completed rentals' });
if (rental.status !== "completed") {
return res
.status(400)
.json({ error: "Can only review completed rentals" });
}
if (rental.itemReviewSubmittedAt) {
return res.status(400).json({ error: 'Item review already submitted' });
return res.status(400).json({ error: "Item review already submitted" });
}
// Submit the review and private message
await rental.update({
itemRating: rating,
await rental.update({
itemRating: rating,
itemReview: review,
itemReviewSubmittedAt: new Date(),
itemPrivateMessage: privateMessage
itemPrivateMessage: privateMessage,
});
// Check and update visibility
@@ -255,7 +295,7 @@ router.post('/:id/review-item', authenticateToken, async (req, res) => {
res.json({
success: true,
reviewVisible: updatedRental.itemReviewVisible
reviewVisible: updatedRental.itemReviewVisible,
});
} catch (error) {
res.status(500).json({ error: error.message });
@@ -263,31 +303,43 @@ router.post('/:id/review-item', authenticateToken, async (req, res) => {
});
// Mark rental as completed (owner only)
router.post('/:id/mark-completed', authenticateToken, async (req, res) => {
router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
try {
console.log('Mark completed endpoint hit for rental ID:', req.params.id);
console.log("Mark completed endpoint hit for rental ID:", req.params.id);
const rental = await Rental.findByPk(req.params.id);
if (!rental) {
return res.status(404).json({ error: 'Rental not found' });
return res.status(404).json({ error: "Rental not found" });
}
if (rental.ownerId !== req.user.id) {
return res.status(403).json({ error: 'Only owners can mark rentals as completed' });
return res
.status(403)
.json({ error: "Only owners can mark rentals as completed" });
}
if (!['active', 'confirmed'].includes(rental.status)) {
return res.status(400).json({ error: 'Can only mark active or confirmed rentals as completed' });
if (!["active", "confirmed"].includes(rental.status)) {
return res.status(400).json({
error: "Can only mark active or confirmed rentals as completed",
});
}
await rental.update({ status: 'completed' });
await rental.update({ status: "completed" });
const updatedRental = await Rental.findByPk(rental.id, {
include: [
{ model: Item, as: 'item' },
{ model: User, as: 'owner', attributes: ['id', 'username', 'firstName', 'lastName'] },
{ model: User, as: 'renter', attributes: ['id', 'username', 'firstName', 'lastName'] }
]
{ model: Item, as: "item" },
{
model: User,
as: "owner",
attributes: ["id", "username", "firstName", "lastName"],
},
{
model: User,
as: "renter",
attributes: ["id", "username", "firstName", "lastName"],
},
],
});
res.json(updatedRental);
@@ -297,21 +349,23 @@ router.post('/:id/mark-completed', authenticateToken, async (req, res) => {
});
// Legacy review endpoint (for backward compatibility)
router.post('/:id/review', authenticateToken, async (req, res) => {
router.post("/:id/review", authenticateToken, async (req, res) => {
try {
const { rating, review } = req.body;
const rental = await Rental.findByPk(req.params.id);
if (!rental) {
return res.status(404).json({ error: 'Rental not found' });
return res.status(404).json({ error: "Rental not found" });
}
if (rental.renterId !== req.user.id) {
return res.status(403).json({ error: 'Only renters can leave reviews' });
return res.status(403).json({ error: "Only renters can leave reviews" });
}
if (rental.status !== 'completed') {
return res.status(400).json({ error: 'Can only review completed rentals' });
if (rental.status !== "completed") {
return res
.status(400)
.json({ error: "Can only review completed rentals" });
}
await rental.update({ rating, review });
@@ -322,4 +376,4 @@ router.post('/:id/review', authenticateToken, async (req, res) => {
}
});
module.exports = router;
module.exports = router;

206
backend/routes/stripe.js Normal file
View File

@@ -0,0 +1,206 @@
const express = require("express");
const { authenticateToken } = require("../middleware/auth");
const { User } = require("../models");
const { Rental, Item } = require("../models");
const StripeService = require("../services/stripeService");
const router = express.Router();
const platformFee = 0.1;
router.post("/create-checkout-session", async (req, res) => {
try {
const { itemName, total, return_url } = req.body;
if (!itemName) {
return res.status(400).json({ error: "No item name found" });
}
if (total == null || total === undefined) {
return res.status(400).json({ error: "No total found" });
}
if (!return_url) {
return res.status(400).json({ error: "No return_url found" });
}
const session = await StripeService.createCheckoutSession({
item_name: itemName,
total: total,
return_url: return_url,
});
res.json({ clientSecret: session.client_secret });
} catch (error) {
console.error("Error creating checkout session:", error);
res.status(500).json({ error: error.message });
}
});
// Get checkout session status
router.get("/checkout-session/:sessionId", async (req, res) => {
try {
const { sessionId } = req.params;
const session = await StripeService.getCheckoutSession(sessionId);
res.json({
status: session.status,
payment_status: session.payment_status,
customer_email: session.customer_details?.email,
});
} catch (error) {
console.error("Error retrieving checkout session:", error);
res.status(500).json({ error: error.message });
}
});
// // Create connected account
// router.post("/accounts", authenticateToken, async (req, res) => {
// try {
// const user = await User.findByPk(req.user.id);
// if (!user) {
// return res.status(404).json({ error: "User not found" });
// }
// // Check if user already has a connected account
// if (user.stripeConnectedAccountId) {
// return res
// .status(400)
// .json({ error: "User already has a connected account" });
// }
// // Create connected account
// const account = await StripeService.createConnectedAccount({
// email: user.email,
// country: "US", // You may want to make this configurable
// });
// // Update user with account ID
// await user.update({
// stripeConnectedAccountId: account.id,
// });
// res.json({
// stripeConnectedAccountId: account.id,
// success: true,
// });
// } catch (error) {
// console.error("Error creating connected account:", error);
// res.status(500).json({ error: error.message });
// }
// });
// // Generate onboarding link
// router.post("/account-links", authenticateToken, async (req, res) => {
// try {
// const user = await User.findByPk(req.user.id);
// if (!user || !user.stripeConnectedAccountId) {
// return res.status(400).json({ error: "No connected account found" });
// }
// const { refreshUrl, returnUrl } = req.body;
// if (!refreshUrl || !returnUrl) {
// return res
// .status(400)
// .json({ error: "refreshUrl and returnUrl are required" });
// }
// const accountLink = await StripeService.createAccountLink(
// user.stripeConnectedAccountId,
// refreshUrl,
// returnUrl
// );
// res.json({
// url: accountLink.url,
// expiresAt: accountLink.expires_at,
// });
// } catch (error) {
// console.error("Error creating account link:", error);
// res.status(500).json({ error: error.message });
// }
// });
// // Get account status
// router.get("/account-status", authenticateToken, async (req, res) => {
// try {
// const user = await User.findByPk(req.user.id);
// if (!user || !user.stripeConnectedAccountId) {
// return res.status(400).json({ error: "No connected account found" });
// }
// const accountStatus = await StripeService.getAccountStatus(
// user.stripeConnectedAccountId
// );
// res.json({
// accountId: accountStatus.id,
// detailsSubmitted: accountStatus.details_submitted,
// payoutsEnabled: accountStatus.payouts_enabled,
// capabilities: accountStatus.capabilities,
// requirements: accountStatus.requirements,
// });
// } catch (error) {
// console.error("Error getting account status:", error);
// res.status(500).json({ error: error.message });
// }
// });
// // Create payment intent for rental
// router.post("/payment-intents", authenticateToken, async (req, res) => {
// try {
// const { rentalId, amount } = req.body;
// if (!rentalId || !amount) {
// return res
// .status(400)
// .json({ error: "rentalId and amount are required" });
// }
// // Get rental details to find owner's connected account
// const rental = await Rental.findByPk(rentalId, {
// include: [{ model: Item, as: "item" }],
// });
// if (!rental) {
// return res.status(404).json({ error: "Rental not found" });
// }
// if (rental.ownerId !== req.user.id) {
// return res.status(403).json({ error: "Unauthorized" });
// }
// // Get owner's connected account
// const owner = await User.findByPk(rental.ownerId);
// if (!owner || !owner.stripeConnectedAccountId) {
// return res
// .status(400)
// .json({ error: "Owner does not have a connected account" });
// }
// const applicationFeeAmount = Math.round(amount * platformFee);
// const paymentIntent = await StripeService.createPaymentIntent({
// amount: Math.round(amount * 100), // Convert to cents
// currency: "usd",
// connectedAccountId: owner.stripeConnectedAccountId,
// applicationFeeAmount: applicationFeeAmount * 100, // Convert to cents
// metadata: {
// rentalId: rental.id,
// renterId: rental.renterId,
// ownerId: owner.id,
// },
// });
// res.json({
// clientSecret: paymentIntent.client_secret,
// paymentIntentId: paymentIntent.id,
// });
// } catch (error) {
// console.error("Error creating payment intent:", error);
// res.status(500).json({ error: error.message });
// }
// });
module.exports = router;