payouts
This commit is contained in:
@@ -1,152 +1,141 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
const { DataTypes } = require("sequelize");
|
||||
const sequelize = require("../config/database");
|
||||
|
||||
const Rental = sequelize.define('Rental', {
|
||||
const Rental = sequelize.define("Rental", {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
primaryKey: true,
|
||||
},
|
||||
itemId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Items',
|
||||
key: 'id'
|
||||
}
|
||||
model: "Items",
|
||||
key: "id",
|
||||
},
|
||||
},
|
||||
renterId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
model: "Users",
|
||||
key: "id",
|
||||
},
|
||||
},
|
||||
ownerId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'Users',
|
||||
key: 'id'
|
||||
}
|
||||
model: "Users",
|
||||
key: "id",
|
||||
},
|
||||
},
|
||||
startDate: {
|
||||
startDateTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
endDate: {
|
||||
endDateTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.STRING
|
||||
},
|
||||
endTime: {
|
||||
type: DataTypes.STRING
|
||||
allowNull: false,
|
||||
},
|
||||
totalAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
baseRentalAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
platformFee: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
processingFee: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
payoutAmount: {
|
||||
type: DataTypes.DECIMAL(10, 2),
|
||||
allowNull: false
|
||||
allowNull: false,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'confirmed', 'active', 'completed', 'cancelled'),
|
||||
defaultValue: 'pending'
|
||||
type: DataTypes.ENUM(
|
||||
"pending",
|
||||
"confirmed",
|
||||
"active",
|
||||
"completed",
|
||||
"cancelled"
|
||||
),
|
||||
defaultValue: "pending",
|
||||
},
|
||||
paymentStatus: {
|
||||
type: DataTypes.ENUM('pending', 'paid', 'refunded'),
|
||||
defaultValue: 'pending'
|
||||
type: DataTypes.ENUM("pending", "paid", "refunded"),
|
||||
defaultValue: "pending",
|
||||
},
|
||||
payoutStatus: {
|
||||
type: DataTypes.ENUM('pending', 'processing', 'completed', 'failed'),
|
||||
defaultValue: 'pending'
|
||||
type: DataTypes.ENUM("pending", "processing", "completed", "failed"),
|
||||
defaultValue: "pending",
|
||||
},
|
||||
payoutProcessedAt: {
|
||||
type: DataTypes.DATE
|
||||
type: DataTypes.DATE,
|
||||
},
|
||||
stripeTransferId: {
|
||||
type: DataTypes.STRING
|
||||
type: DataTypes.STRING,
|
||||
},
|
||||
deliveryMethod: {
|
||||
type: DataTypes.ENUM('pickup', 'delivery'),
|
||||
defaultValue: 'pickup'
|
||||
type: DataTypes.ENUM("pickup", "delivery"),
|
||||
defaultValue: "pickup",
|
||||
},
|
||||
deliveryAddress: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
notes: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
// Renter's review of the item (existing fields renamed for clarity)
|
||||
itemRating: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
itemReview: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
itemReviewSubmittedAt: {
|
||||
type: DataTypes.DATE
|
||||
type: DataTypes.DATE,
|
||||
},
|
||||
itemReviewVisible: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
// Owner's review of the renter
|
||||
renterRating: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
max: 5,
|
||||
},
|
||||
},
|
||||
renterReview: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
renterReviewSubmittedAt: {
|
||||
type: DataTypes.DATE
|
||||
type: DataTypes.DATE,
|
||||
},
|
||||
renterReviewVisible: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
defaultValue: false,
|
||||
},
|
||||
// Private messages (always visible to recipient)
|
||||
itemPrivateMessage: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
renterPrivateMessage: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
// Legacy fields for backwards compatibility
|
||||
rating: {
|
||||
type: DataTypes.INTEGER,
|
||||
validate: {
|
||||
min: 1,
|
||||
max: 5
|
||||
}
|
||||
},
|
||||
review: {
|
||||
type: DataTypes.TEXT
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Rental;
|
||||
module.exports = Rental;
|
||||
|
||||
@@ -98,13 +98,12 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
itemId,
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime,
|
||||
startDateTime,
|
||||
endDateTime,
|
||||
deliveryMethod,
|
||||
deliveryAddress,
|
||||
notes,
|
||||
paymentStatus,
|
||||
} = req.body;
|
||||
|
||||
const item = await Item.findByPk(itemId);
|
||||
@@ -116,16 +115,57 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
return res.status(400).json({ error: "Item is not available" });
|
||||
}
|
||||
|
||||
let rentalStartDateTime, rentalEndDateTime, baseRentalAmount;
|
||||
|
||||
// New UTC datetime format
|
||||
rentalStartDateTime = new Date(startDateTime);
|
||||
rentalEndDateTime = new Date(endDateTime);
|
||||
|
||||
// Calculate rental duration
|
||||
const diffMs = rentalEndDateTime.getTime() - rentalStartDateTime.getTime();
|
||||
const diffHours = Math.ceil(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Calculate base amount based on duration
|
||||
if (item.pricePerHour && diffHours <= 24) {
|
||||
baseRentalAmount = diffHours * Number(item.pricePerHour);
|
||||
} else if (item.pricePerDay) {
|
||||
baseRentalAmount = diffDays * Number(item.pricePerDay);
|
||||
} else {
|
||||
baseRentalAmount = 0;
|
||||
}
|
||||
|
||||
// Check for overlapping rentals using datetime ranges
|
||||
const overlappingRental = await Rental.findOne({
|
||||
where: {
|
||||
itemId,
|
||||
status: { [Op.in]: ["confirmed", "active"] },
|
||||
[Op.or]: [
|
||||
{
|
||||
startDate: { [Op.between]: [startDate, endDate] },
|
||||
},
|
||||
{
|
||||
endDate: { [Op.between]: [startDate, endDate] },
|
||||
[Op.and]: [
|
||||
{ startDateTime: { [Op.not]: null } },
|
||||
{ endDateTime: { [Op.not]: null } },
|
||||
{
|
||||
[Op.or]: [
|
||||
{
|
||||
startDateTime: {
|
||||
[Op.between]: [rentalStartDateTime, rentalEndDateTime],
|
||||
},
|
||||
},
|
||||
{
|
||||
endDateTime: {
|
||||
[Op.between]: [rentalStartDateTime, rentalEndDateTime],
|
||||
},
|
||||
},
|
||||
{
|
||||
[Op.and]: [
|
||||
{ startDateTime: { [Op.lte]: rentalStartDateTime } },
|
||||
{ endDateTime: { [Op.gte]: rentalEndDateTime } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -137,11 +177,6 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
.json({ error: "Item is already booked for these dates" });
|
||||
}
|
||||
|
||||
const rentalDays = Math.ceil(
|
||||
(new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
const baseRentalAmount = rentalDays * (item.pricePerDay || 0);
|
||||
|
||||
// Calculate fees using FeeCalculator
|
||||
const fees = FeeCalculator.calculateRentalFees(baseRentalAmount);
|
||||
|
||||
@@ -149,15 +184,14 @@ router.post("/", authenticateToken, async (req, res) => {
|
||||
itemId,
|
||||
renterId: req.user.id,
|
||||
ownerId: item.ownerId,
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime,
|
||||
startDateTime: rentalStartDateTime,
|
||||
endDateTime: rentalEndDateTime,
|
||||
totalAmount: fees.totalChargedAmount,
|
||||
baseRentalAmount: fees.baseRentalAmount,
|
||||
platformFee: fees.platformFee,
|
||||
processingFee: fees.processingFee,
|
||||
payoutAmount: fees.payoutAmount,
|
||||
paymentStatus: paymentStatus || "pending",
|
||||
deliveryMethod,
|
||||
deliveryAddress,
|
||||
notes,
|
||||
@@ -356,34 +390,6 @@ router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy review endpoint (for backward compatibility)
|
||||
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" });
|
||||
}
|
||||
|
||||
if (rental.renterId !== req.user.id) {
|
||||
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" });
|
||||
}
|
||||
|
||||
await rental.update({ rating, review });
|
||||
|
||||
res.json(rental);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate fees for rental pricing display
|
||||
router.post("/calculate-fees", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
@@ -406,8 +412,8 @@ router.post("/calculate-fees", authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get payout status for owner's rentals
|
||||
router.get("/payouts/status", authenticateToken, async (req, res) => {
|
||||
// Get earnings status for owner's rentals
|
||||
router.get("/earnings/status", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const ownerRentals = await Rental.findAll({
|
||||
where: {
|
||||
@@ -429,7 +435,7 @@ router.get("/payouts/status", authenticateToken, async (req, res) => {
|
||||
|
||||
res.json(ownerRentals);
|
||||
} catch (error) {
|
||||
console.error("Error getting payout status:", error);
|
||||
console.error("Error getting earnings status:", error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
const express = require("express");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const { User } = require("../models");
|
||||
const { Rental, Item } = require("../models");
|
||||
const { User, Item } = require("../models");
|
||||
const StripeService = require("../services/stripeService");
|
||||
const router = express.Router();
|
||||
const platformFee = 0.1;
|
||||
|
||||
router.post("/create-checkout-session", async (req, res) => {
|
||||
router.post("/create-checkout-session", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { itemName, total, return_url, rentalData } = req.body;
|
||||
|
||||
@@ -20,18 +18,37 @@ router.post("/create-checkout-session", async (req, res) => {
|
||||
return res.status(400).json({ error: "No return_url found" });
|
||||
}
|
||||
|
||||
// Validate rental data and user authorization
|
||||
if (rentalData && rentalData.itemId) {
|
||||
const item = await Item.findByPk(rentalData.itemId);
|
||||
|
||||
if (!item) {
|
||||
return res.status(404).json({ error: "Item not found" });
|
||||
}
|
||||
|
||||
if (!item.availability) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Item is not available for rent" });
|
||||
}
|
||||
|
||||
// Check if user is trying to rent their own item
|
||||
if (item.ownerId === req.user.id) {
|
||||
return res.status(400).json({ error: "You cannot rent your own item" });
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare metadata - Stripe metadata keys must be strings
|
||||
const metadata = rentalData
|
||||
? {
|
||||
itemId: rentalData.itemId,
|
||||
startDate: rentalData.startDate,
|
||||
endDate: rentalData.endDate,
|
||||
startTime: rentalData.startTime,
|
||||
endTime: rentalData.endTime,
|
||||
renterId: req.user.id.toString(), // Add authenticated user ID
|
||||
startDateTime: rentalData.startDateTime,
|
||||
endDateTime: rentalData.endDateTime,
|
||||
totalAmount: rentalData.totalAmount.toString(),
|
||||
deliveryMethod: rentalData.deliveryMethod,
|
||||
}
|
||||
: {};
|
||||
: { renterId: req.user.id.toString() };
|
||||
|
||||
const session = await StripeService.createCheckoutSession({
|
||||
item_name: itemName,
|
||||
|
||||
@@ -59,6 +59,8 @@ class PayoutService {
|
||||
ownerId: rental.ownerId,
|
||||
baseAmount: rental.baseRentalAmount.toString(),
|
||||
platformFee: rental.platformFee.toString(),
|
||||
startDateTime: rental.startDateTime.toISOString(),
|
||||
endDateTime: rental.endDateTime.toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class StripeService {
|
||||
static async createConnectedAccount({ email, country = "US" }) {
|
||||
try {
|
||||
const account = await stripe.accounts.create({
|
||||
type: "standard",
|
||||
type: "express",
|
||||
email,
|
||||
country,
|
||||
capabilities: {
|
||||
|
||||
Reference in New Issue
Block a user