diff --git a/backend/jobs/payoutProcessor.js b/backend/jobs/payoutProcessor.js
index 9982788..f55f42c 100644
--- a/backend/jobs/payoutProcessor.js
+++ b/backend/jobs/payoutProcessor.js
@@ -1,7 +1,7 @@
const cron = require("node-cron");
const PayoutService = require("../services/payoutService");
-const paymentsSchedule = "31 * * * *"; // Run every hour at minute 0
+const paymentsSchedule = "0 * * * *"; // Run every hour at minute 0
const retrySchedule = "0 7 * * *"; // Retry failed payouts once daily at 7 AM
class PayoutProcessor {
diff --git a/backend/models/Rental.js b/backend/models/Rental.js
index 003f410..a1784c5 100644
--- a/backend/models/Rental.js
+++ b/backend/models/Rental.js
@@ -43,18 +43,10 @@ const Rental = sequelize.define("Rental", {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
},
- baseRentalAmount: {
- type: DataTypes.DECIMAL(10, 2),
- allowNull: false,
- },
platformFee: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
},
- processingFee: {
- type: DataTypes.DECIMAL(10, 2),
- allowNull: false,
- },
payoutAmount: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false,
@@ -83,6 +75,28 @@ const Rental = sequelize.define("Rental", {
stripeTransferId: {
type: DataTypes.STRING,
},
+ // Refund tracking fields
+ refundAmount: {
+ type: DataTypes.DECIMAL(10, 2),
+ },
+ refundProcessedAt: {
+ type: DataTypes.DATE,
+ },
+ refundReason: {
+ type: DataTypes.TEXT,
+ },
+ stripeRefundId: {
+ type: DataTypes.STRING,
+ },
+ cancelledBy: {
+ type: DataTypes.ENUM("renter", "owner"),
+ },
+ cancelledAt: {
+ type: DataTypes.DATE,
+ },
+ stripePaymentMethodId: {
+ type: DataTypes.STRING,
+ },
deliveryMethod: {
type: DataTypes.ENUM("pickup", "delivery"),
defaultValue: "pickup",
diff --git a/backend/models/User.js b/backend/models/User.js
index ae36aa0..4897df2 100644
--- a/backend/models/User.js
+++ b/backend/models/User.js
@@ -102,6 +102,10 @@ const User = sequelize.define('User', {
stripeConnectedAccountId: {
type: DataTypes.STRING,
allowNull: true
+ },
+ stripeCustomerId: {
+ type: DataTypes.STRING,
+ allowNull: true
}
}, {
hooks: {
diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js
index 30594a5..16401c8 100644
--- a/backend/routes/rentals.js
+++ b/backend/routes/rentals.js
@@ -3,6 +3,7 @@ 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 FeeCalculator = require("../utils/feeCalculator");
+const RefundService = require("../services/refundService");
const router = express.Router();
// Helper function to check and update review visibility
@@ -103,7 +104,7 @@ router.post("/", authenticateToken, async (req, res) => {
deliveryMethod,
deliveryAddress,
notes,
- paymentStatus,
+ stripePaymentMethodId,
} = req.body;
const item = await Item.findByPk(itemId);
@@ -115,7 +116,7 @@ router.post("/", authenticateToken, async (req, res) => {
return res.status(400).json({ error: "Item is not available" });
}
- let rentalStartDateTime, rentalEndDateTime, baseRentalAmount;
+ let rentalStartDateTime, rentalEndDateTime, totalAmount;
// New UTC datetime format
rentalStartDateTime = new Date(startDateTime);
@@ -128,11 +129,11 @@ router.post("/", authenticateToken, async (req, res) => {
// Calculate base amount based on duration
if (item.pricePerHour && diffHours <= 24) {
- baseRentalAmount = diffHours * Number(item.pricePerHour);
+ totalAmount = diffHours * Number(item.pricePerHour);
} else if (item.pricePerDay) {
- baseRentalAmount = diffDays * Number(item.pricePerDay);
+ totalAmount = diffDays * Number(item.pricePerDay);
} else {
- baseRentalAmount = 0;
+ totalAmount = 0;
}
// Check for overlapping rentals using datetime ranges
@@ -178,7 +179,12 @@ router.post("/", authenticateToken, async (req, res) => {
}
// Calculate fees using FeeCalculator
- const fees = FeeCalculator.calculateRentalFees(baseRentalAmount);
+ const fees = FeeCalculator.calculateRentalFees(totalAmount);
+
+ // Validate that payment method was provided
+ if (!stripePaymentMethodId) {
+ return res.status(400).json({ error: "Payment method is required" });
+ }
const rental = await Rental.create({
itemId,
@@ -187,14 +193,14 @@ router.post("/", authenticateToken, async (req, res) => {
startDateTime: rentalStartDateTime,
endDateTime: rentalEndDateTime,
totalAmount: fees.totalChargedAmount,
- baseRentalAmount: fees.baseRentalAmount,
platformFee: fees.platformFee,
- processingFee: fees.processingFee,
payoutAmount: fees.payoutAmount,
- paymentStatus: paymentStatus || "pending",
+ paymentStatus: "pending",
+ status: "pending",
deliveryMethod,
deliveryAddress,
notes,
+ stripePaymentMethodId,
});
const rentalWithDetails = await Rental.findByPk(rental.id, {
@@ -222,7 +228,21 @@ router.post("/", 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);
+ const rental = await Rental.findByPk(req.params.id, {
+ include: [
+ { model: Item, as: "item" },
+ {
+ model: User,
+ as: "owner",
+ attributes: ["id", "username", "firstName", "lastName"],
+ },
+ {
+ model: User,
+ as: "renter",
+ attributes: ["id", "username", "firstName", "lastName", "stripeCustomerId"],
+ },
+ ],
+ });
if (!rental) {
return res.status(404).json({ error: "Rental not found" });
@@ -232,6 +252,69 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
return res.status(403).json({ error: "Unauthorized" });
}
+ // If owner is approving a pending rental, charge the stored payment method
+ if (status === "confirmed" && rental.status === "pending" && rental.ownerId === req.user.id) {
+ if (!rental.stripePaymentMethodId) {
+ return res.status(400).json({ error: "No payment method found for this rental" });
+ }
+
+ try {
+ // Import StripeService to process the payment
+ const StripeService = require("../services/stripeService");
+
+ // Check if renter has a stripe customer ID
+ if (!rental.renter.stripeCustomerId) {
+ return res.status(400).json({ error: "Renter does not have a Stripe customer account" });
+ }
+
+ // Create payment intent and charge the stored payment method
+ const paymentResult = await StripeService.chargePaymentMethod(
+ rental.stripePaymentMethodId,
+ rental.totalAmount,
+ rental.renter.stripeCustomerId,
+ {
+ rentalId: rental.id,
+ itemName: rental.item.name,
+ renterId: rental.renterId,
+ ownerId: rental.ownerId,
+ }
+ );
+
+ // Update rental with payment completion
+ await rental.update({
+ status: "confirmed",
+ paymentStatus: "paid",
+ stripePaymentIntentId: paymentResult.paymentIntentId,
+ });
+
+ 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"],
+ },
+ ],
+ });
+
+ res.json(updatedRental);
+ return;
+ } catch (paymentError) {
+ console.error("Payment failed during approval:", paymentError);
+ // Keep rental as pending, but inform of payment failure
+ return res.status(400).json({
+ error: "Payment failed during approval",
+ details: paymentError.message
+ });
+ }
+ }
+
await rental.update({ status });
const updatedRental = await Rental.findByPk(rental.id, {
@@ -393,13 +476,13 @@ router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
// Calculate fees for rental pricing display
router.post("/calculate-fees", authenticateToken, async (req, res) => {
try {
- const { baseAmount } = req.body;
+ const { totalAmount } = req.body;
- if (!baseAmount || baseAmount <= 0) {
+ if (!totalAmount || totalAmount <= 0) {
return res.status(400).json({ error: "Valid base amount is required" });
}
- const fees = FeeCalculator.calculateRentalFees(baseAmount);
+ const fees = FeeCalculator.calculateRentalFees(totalAmount);
const displayFees = FeeCalculator.formatFeesForDisplay(fees);
res.json({
@@ -422,7 +505,7 @@ router.get("/earnings/status", authenticateToken, async (req, res) => {
},
attributes: [
"id",
- "baseRentalAmount",
+ "totalAmount",
"platformFee",
"payoutAmount",
"payoutStatus",
@@ -440,4 +523,56 @@ router.get("/earnings/status", authenticateToken, async (req, res) => {
}
});
+// Get refund preview (what would happen if cancelled now)
+router.get("/:id/refund-preview", authenticateToken, async (req, res) => {
+ try {
+ const preview = await RefundService.getRefundPreview(
+ req.params.id,
+ req.user.id
+ );
+ res.json(preview);
+ } catch (error) {
+ console.error("Error getting refund preview:", error);
+ res.status(400).json({ error: error.message });
+ }
+});
+
+// Cancel rental with refund processing
+router.post("/:id/cancel", authenticateToken, async (req, res) => {
+ try {
+ const { reason } = req.body;
+
+ const result = await RefundService.processCancellation(
+ req.params.id,
+ req.user.id,
+ reason
+ );
+
+ // Return the updated rental with refund information
+ const updatedRental = await Rental.findByPk(result.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"],
+ },
+ ],
+ });
+
+ res.json({
+ rental: updatedRental,
+ refund: result.refund,
+ });
+ } catch (error) {
+ console.error("Error cancelling rental:", error);
+ res.status(400).json({ error: error.message });
+ }
+});
+
module.exports = router;
diff --git a/backend/routes/stripe.js b/backend/routes/stripe.js
index 14d5622..03f175f 100644
--- a/backend/routes/stripe.js
+++ b/backend/routes/stripe.js
@@ -4,66 +4,6 @@ const { User, Item } = require("../models");
const StripeService = require("../services/stripeService");
const router = express.Router();
-router.post("/create-checkout-session", authenticateToken, async (req, res) => {
- try {
- const { itemName, total, return_url, rentalData } = 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" });
- }
-
- // 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,
- 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,
- total: total,
- return_url: return_url,
- metadata: metadata,
- });
-
- 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 {
@@ -75,6 +15,7 @@ router.get("/checkout-session/:sessionId", async (req, res) => {
status: session.status,
payment_status: session.payment_status,
customer_email: session.customer_details?.email,
+ setup_intent: session.setup_intent,
metadata: session.metadata,
});
} catch (error) {
@@ -179,4 +120,54 @@ router.get("/account-status", authenticateToken, async (req, res) => {
}
});
+// Create embedded setup checkout session for collecting payment method
+router.post("/create-setup-checkout-session", authenticateToken, async (req, res) => {
+ try {
+ const { rentalData } = req.body;
+
+ const user = await User.findByPk(req.user.id);
+
+ if (!user) {
+ return res.status(404).json({ error: "User not found" });
+ }
+
+ // Create or get Stripe customer
+ let stripeCustomerId = user.stripeCustomerId;
+
+ if (!stripeCustomerId) {
+ // Create new Stripe customer
+ const customer = await StripeService.createCustomer({
+ email: user.email,
+ name: `${user.firstName} ${user.lastName}`,
+ metadata: {
+ userId: user.id.toString()
+ }
+ });
+
+ stripeCustomerId = customer.id;
+
+ // Save customer ID to user record
+ await user.update({ stripeCustomerId });
+ }
+
+ // Add rental data to metadata if provided
+ const metadata = rentalData ? {
+ rentalData: JSON.stringify(rentalData)
+ } : {};
+
+ const session = await StripeService.createSetupCheckoutSession({
+ customerId: stripeCustomerId,
+ metadata
+ });
+
+ res.json({
+ clientSecret: session.client_secret,
+ sessionId: session.id
+ });
+ } catch (error) {
+ console.error("Error creating setup checkout session:", error);
+ res.status(500).json({ error: error.message });
+ }
+});
+
module.exports = router;
diff --git a/backend/services/payoutService.js b/backend/services/payoutService.js
index ea057dc..88d7f8a 100644
--- a/backend/services/payoutService.js
+++ b/backend/services/payoutService.js
@@ -57,7 +57,7 @@ class PayoutService {
metadata: {
rentalId: rental.id,
ownerId: rental.ownerId,
- baseAmount: rental.baseRentalAmount.toString(),
+ totalAmount: rental.totalAmount.toString(),
platformFee: rental.platformFee.toString(),
startDateTime: rental.startDateTime.toISOString(),
endDateTime: rental.endDateTime.toISOString(),
diff --git a/backend/services/refundService.js b/backend/services/refundService.js
new file mode 100644
index 0000000..af6b01b
--- /dev/null
+++ b/backend/services/refundService.js
@@ -0,0 +1,229 @@
+const { Rental } = require("../models");
+const StripeService = require("./stripeService");
+
+class RefundService {
+ /**
+ * Calculate refund amount based on policy and who cancelled
+ * @param {Object} rental - Rental instance
+ * @param {string} cancelledBy - 'renter' or 'owner'
+ * @returns {Object} - { refundAmount, refundPercentage, reason }
+ */
+ static calculateRefundAmount(rental, cancelledBy) {
+ const totalAmount = rental.totalAmount;
+ let refundPercentage = 0;
+ let reason = "";
+
+ if (cancelledBy === "owner") {
+ // Owner cancellation = full refund
+ refundPercentage = 1.0;
+ reason = "Full refund - cancelled by owner";
+ } else if (cancelledBy === "renter") {
+ // Calculate based on time until rental start
+ const now = new Date();
+ const startDateTime = new Date(rental.startDateTime);
+ const hoursUntilStart = (startDateTime - now) / (1000 * 60 * 60);
+
+ if (hoursUntilStart < 24) {
+ refundPercentage = 0.0;
+ reason = "No refund - cancelled within 24 hours of start time";
+ } else if (hoursUntilStart < 48) {
+ refundPercentage = 0.5;
+ reason = "50% refund - cancelled between 24-48 hours of start time";
+ } else {
+ refundPercentage = 1.0;
+ reason = "Full refund - cancelled more than 48 hours before start time";
+ }
+ }
+
+ const refundAmount = parseFloat((totalAmount * refundPercentage).toFixed(2));
+
+ return {
+ refundAmount,
+ refundPercentage,
+ reason,
+ };
+ }
+
+ /**
+ * Validate if a rental can be cancelled
+ * @param {Object} rental - Rental instance
+ * @param {string} userId - User ID attempting to cancel
+ * @returns {Object} - { canCancel, reason, cancelledBy }
+ */
+ static validateCancellationEligibility(rental, userId) {
+ // Check if rental is already cancelled
+ if (rental.status === "cancelled") {
+ return {
+ canCancel: false,
+ reason: "Rental is already cancelled",
+ cancelledBy: null,
+ };
+ }
+
+ // Check if rental is completed
+ if (rental.status === "completed") {
+ return {
+ canCancel: false,
+ reason: "Cannot cancel completed rental",
+ cancelledBy: null,
+ };
+ }
+
+ // Check if rental is active
+ if (rental.status === "active") {
+ return {
+ canCancel: false,
+ reason: "Cannot cancel active rental",
+ cancelledBy: null,
+ };
+ }
+
+ // Check if user has permission to cancel
+ let cancelledBy = null;
+ if (rental.renterId === userId) {
+ cancelledBy = "renter";
+ } else if (rental.ownerId === userId) {
+ cancelledBy = "owner";
+ } else {
+ return {
+ canCancel: false,
+ reason: "You are not authorized to cancel this rental",
+ cancelledBy: null,
+ };
+ }
+
+ // Check payment status
+ if (rental.paymentStatus !== "paid") {
+ return {
+ canCancel: false,
+ reason: "Cannot cancel rental that hasn't been paid",
+ cancelledBy: null,
+ };
+ }
+
+ return {
+ canCancel: true,
+ reason: "Cancellation allowed",
+ cancelledBy,
+ };
+ }
+
+ /**
+ * Process the full cancellation and refund
+ * @param {string} rentalId - Rental ID
+ * @param {string} userId - User ID cancelling
+ * @param {string} cancellationReason - Optional reason provided by user
+ * @returns {Object} - Updated rental with refund information
+ */
+ static async processCancellation(rentalId, userId, cancellationReason = null) {
+ const rental = await Rental.findByPk(rentalId);
+
+ if (!rental) {
+ throw new Error("Rental not found");
+ }
+
+ // Validate cancellation eligibility
+ const eligibility = this.validateCancellationEligibility(rental, userId);
+ if (!eligibility.canCancel) {
+ throw new Error(eligibility.reason);
+ }
+
+ // Calculate refund amount
+ const refundCalculation = this.calculateRefundAmount(
+ rental,
+ eligibility.cancelledBy
+ );
+
+ let stripeRefundId = null;
+ let refundProcessedAt = null;
+
+ // Process refund with Stripe if amount > 0 and we have payment intent ID
+ if (
+ refundCalculation.refundAmount > 0 &&
+ rental.stripePaymentIntentId
+ ) {
+ try {
+ const refund = await StripeService.createRefund({
+ paymentIntentId: rental.stripePaymentIntentId,
+ amount: refundCalculation.refundAmount,
+ metadata: {
+ rentalId: rental.id,
+ cancelledBy: eligibility.cancelledBy,
+ refundReason: refundCalculation.reason,
+ },
+ });
+
+ stripeRefundId = refund.id;
+ refundProcessedAt = new Date();
+ } catch (error) {
+ console.error("Error processing Stripe refund:", error);
+ throw new Error(`Failed to process refund: ${error.message}`);
+ }
+ } else if (refundCalculation.refundAmount > 0) {
+ // Log warning if we should refund but don't have payment intent
+ console.warn(
+ `Refund amount calculated but no payment intent ID for rental ${rentalId}`
+ );
+ }
+
+ // Update rental with cancellation and refund info
+ const updatedRental = await rental.update({
+ status: "cancelled",
+ cancelledBy: eligibility.cancelledBy,
+ cancelledAt: new Date(),
+ refundAmount: refundCalculation.refundAmount,
+ refundProcessedAt,
+ refundReason:
+ cancellationReason || refundCalculation.reason,
+ stripeRefundId,
+ // Reset payout status since rental is cancelled
+ payoutStatus: "pending",
+ });
+
+ return {
+ rental: updatedRental,
+ refund: {
+ amount: refundCalculation.refundAmount,
+ percentage: refundCalculation.refundPercentage,
+ reason: refundCalculation.reason,
+ processed: !!refundProcessedAt,
+ stripeRefundId,
+ },
+ };
+ }
+
+ /**
+ * Get refund preview without processing
+ * @param {string} rentalId - Rental ID
+ * @param {string} userId - User ID requesting preview
+ * @returns {Object} - Preview of refund calculation
+ */
+ static async getRefundPreview(rentalId, userId) {
+ const rental = await Rental.findByPk(rentalId);
+
+ if (!rental) {
+ throw new Error("Rental not found");
+ }
+
+ const eligibility = this.validateCancellationEligibility(rental, userId);
+ if (!eligibility.canCancel) {
+ throw new Error(eligibility.reason);
+ }
+
+ const refundCalculation = this.calculateRefundAmount(
+ rental,
+ eligibility.cancelledBy
+ );
+
+ return {
+ canCancel: true,
+ cancelledBy: eligibility.cancelledBy,
+ refundAmount: refundCalculation.refundAmount,
+ refundPercentage: refundCalculation.refundPercentage,
+ reason: refundCalculation.reason,
+ totalAmount: rental.totalAmount,
+ };
+ }
+}
+
+module.exports = RefundService;
\ No newline at end of file
diff --git a/backend/services/stripeService.js b/backend/services/stripeService.js
index 2892ad0..ce46b6a 100644
--- a/backend/services/stripeService.js
+++ b/backend/services/stripeService.js
@@ -1,44 +1,12 @@
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
class StripeService {
- static async createCheckoutSession({
- item_name,
- total,
- return_url,
- metadata = {},
- }) {
- try {
- const sessionConfig = {
- line_items: [
- {
- price_data: {
- currency: "usd",
- product_data: {
- name: item_name,
- },
- unit_amount: total * 100,
- },
- quantity: 1,
- },
- ],
- mode: "payment",
- ui_mode: "embedded",
- return_url: return_url,
- metadata: metadata,
- };
-
- const session = await stripe.checkout.sessions.create(sessionConfig);
-
- return session;
- } catch (error) {
- console.error("Error creating checkout session:", error);
- throw error;
- }
- }
static async getCheckoutSession(sessionId) {
try {
- return await stripe.checkout.sessions.retrieve(sessionId);
+ return await stripe.checkout.sessions.retrieve(sessionId, {
+ expand: ['setup_intent', 'setup_intent.payment_method']
+ });
} catch (error) {
console.error("Error retrieving checkout session:", error);
throw error;
@@ -115,6 +83,97 @@ class StripeService {
throw error;
}
}
+
+ static async createRefund({
+ paymentIntentId,
+ amount,
+ metadata = {},
+ reason = "requested_by_customer",
+ }) {
+ try {
+ const refund = await stripe.refunds.create({
+ payment_intent: paymentIntentId,
+ amount: Math.round(amount * 100), // Convert to cents
+ metadata,
+ reason,
+ });
+
+ return refund;
+ } catch (error) {
+ console.error("Error creating refund:", error);
+ throw error;
+ }
+ }
+
+ static async getRefund(refundId) {
+ try {
+ return await stripe.refunds.retrieve(refundId);
+ } catch (error) {
+ console.error("Error retrieving refund:", error);
+ throw error;
+ }
+ }
+
+ static async chargePaymentMethod(paymentMethodId, amount, customerId, metadata = {}) {
+ try {
+ // Create a payment intent with the stored payment method
+ const paymentIntent = await stripe.paymentIntents.create({
+ amount: Math.round(amount * 100), // Convert to cents
+ currency: "usd",
+ payment_method: paymentMethodId,
+ customer: customerId, // Include customer ID
+ confirm: true, // Automatically confirm the payment
+ return_url: `${process.env.FRONTEND_URL || 'http://localhost:3000'}/payment-complete`,
+ metadata,
+ });
+
+ return {
+ paymentIntentId: paymentIntent.id,
+ status: paymentIntent.status,
+ clientSecret: paymentIntent.client_secret,
+ };
+ } catch (error) {
+ console.error("Error charging payment method:", error);
+ throw error;
+ }
+ }
+
+ static async createCustomer({ email, name, metadata = {} }) {
+ try {
+ const customer = await stripe.customers.create({
+ email,
+ name,
+ metadata,
+ });
+
+ return customer;
+ } catch (error) {
+ console.error("Error creating customer:", error);
+ throw error;
+ }
+ }
+
+
+ static async createSetupCheckoutSession({ customerId, metadata = {} }) {
+ try {
+ const session = await stripe.checkout.sessions.create({
+ customer: customerId,
+ payment_method_types: ['card', 'us_bank_account', 'link'],
+ mode: 'setup',
+ ui_mode: 'embedded',
+ redirect_on_completion: 'never',
+ metadata: {
+ type: 'payment_method_setup',
+ ...metadata
+ }
+ });
+
+ return session;
+ } catch (error) {
+ console.error("Error creating setup checkout session:", error);
+ throw error;
+ }
+ }
}
module.exports = StripeService;
diff --git a/backend/utils/feeCalculator.js b/backend/utils/feeCalculator.js
index 9c0e992..b638585 100644
--- a/backend/utils/feeCalculator.js
+++ b/backend/utils/feeCalculator.js
@@ -1,26 +1,21 @@
class FeeCalculator {
- static calculateRentalFees(baseAmount) {
+ static calculateRentalFees(totalAmount) {
const platformFeeRate = 0.2;
- const stripeRate = 0.029;
- const stripeFixedFee = 0.3;
- const platformFee = baseAmount * platformFeeRate;
- const processingFee = baseAmount * stripeRate + stripeFixedFee;
+ const platformFee = totalAmount * platformFeeRate;
return {
- baseRentalAmount: parseFloat(baseAmount.toFixed(2)),
+ totalAmount: parseFloat(totalAmount.toFixed(2)),
platformFee: parseFloat(platformFee.toFixed(2)),
- processingFee: parseFloat(processingFee.toFixed(2)),
- totalChargedAmount: parseFloat((baseAmount + processingFee).toFixed(2)),
- payoutAmount: parseFloat((baseAmount - platformFee).toFixed(2)),
+ totalChargedAmount: parseFloat(totalAmount.toFixed(2)),
+ payoutAmount: parseFloat((totalAmount - platformFee).toFixed(2)),
};
}
static formatFeesForDisplay(fees) {
return {
- baseRental: `$${fees.baseRentalAmount.toFixed(2)}`,
+ totalAmount: `$${fees.totalAmount.toFixed(2)}`,
platformFee: `$${fees.platformFee.toFixed(2)} (20%)`,
- processingFee: `$${fees.processingFee.toFixed(2)} (2.9% + $0.30)`,
totalCharge: `$${fees.totalChargedAmount.toFixed(2)}`,
ownerPayout: `$${fees.payoutAmount.toFixed(2)}`,
};
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 0d354f4..400051e 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -23,7 +23,6 @@ import ItemRequestDetail from './pages/ItemRequestDetail';
import CreateItemRequest from './pages/CreateItemRequest';
import MyRequests from './pages/MyRequests';
import EarningsDashboard from './pages/EarningsDashboard';
-import CheckoutReturn from './components/CheckoutReturn';
import PrivateRoute from './components/PrivateRoute';
import './App.css';
@@ -131,14 +130,6 @@ function App() {
- Please wait while we confirm your payment and set up your rental.
-
- Your rental has been confirmed. You can view the details in your
- rentals page.
-
- Your payment was processed successfully, but we encountered an
- issue creating your rental record:
-
- {error || "There was an issue processing your payment."}
- Preparing secure checkout... Unable to load checkout
+ Item: {rental.item?.name}
+
+ Start:{" "}
+ {new Date(rental.startDateTime).toLocaleString()}
+
+ End:{" "}
+ {new Date(rental.endDateTime).toLocaleString()}
+
+ Total Amount:{" "}
+ {formatCurrency(rental.totalAmount)}
+ Preparing payment...Processing your payment...
- Payment Successful!
- Payment Successful - Rental Setup Issue
-
- {error}
-
- {status === "failed" ? "Payment Incomplete" : "Payment Error"}
-
- Cancel Rental
+
+ Rental Details
+ Refund Information
+
+ {refundPreview.reason}
+
+ Your rental request has been submitted to the owner. + You'll only be charged if they approve your request. +
++ Add your payment method to complete your rental request. + You'll only be charged if the owner approves your request. +
+ + {!manualSelection.startDate || !manualSelection.endDate || !getRentalData() ? ( +