From b52104c3fadc33f1a4c0f618f9e6dde5f47193ea Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Fri, 29 Aug 2025 00:32:06 -0400 Subject: [PATCH] started payouts --- .gitignore | 1 + backend/jobs/payoutProcessor.js | 90 +++++++++++++ backend/models/Rental.js | 26 ++++ backend/models/User.js | 4 + backend/package-lock.json | 22 +++ backend/package.json | 1 + backend/routes/rentals.js | 62 ++++++++- backend/routes/stripe.js | 216 +++++++++++------------------- backend/server.js | 6 + backend/services/payoutService.js | 194 +++++++++++++++++++++++++++ backend/services/stripeService.js | 165 +++++++++-------------- backend/utils/feeCalculator.js | 30 +++++ frontend/src/pages/Profile.tsx | 13 -- 13 files changed, 578 insertions(+), 252 deletions(-) create mode 100644 backend/jobs/payoutProcessor.js create mode 100644 backend/services/payoutService.js create mode 100644 backend/utils/feeCalculator.js diff --git a/.gitignore b/.gitignore index c0c36e1..5d89c4e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ node_modules/ .env.development.local .env.test.local .env.production.local +.mcp.json # Logs npm-debug.log* diff --git a/backend/jobs/payoutProcessor.js b/backend/jobs/payoutProcessor.js new file mode 100644 index 0000000..9982788 --- /dev/null +++ b/backend/jobs/payoutProcessor.js @@ -0,0 +1,90 @@ +const cron = require("node-cron"); +const PayoutService = require("../services/payoutService"); + +const paymentsSchedule = "31 * * * *"; // Run every hour at minute 0 +const retrySchedule = "0 7 * * *"; // Retry failed payouts once daily at 7 AM + +class PayoutProcessor { + static startScheduledPayouts() { + console.log("Starting automated payout processor..."); + + const payoutJob = cron.schedule( + paymentsSchedule, + async () => { + console.log("Running scheduled payout processing..."); + + try { + const results = await PayoutService.processAllEligiblePayouts(); + + if (results.totalProcessed > 0) { + console.log( + `Payout batch completed: ${results.successful.length} successful, ${results.failed.length} failed` + ); + + // Log any failures for monitoring + if (results.failed.length > 0) { + console.warn("Failed payouts:", results.failed); + } + } + } catch (error) { + console.error("Error in scheduled payout processing:", error); + } + }, + { + scheduled: false, + timezone: "America/New_York", + } + ); + + const retryJob = cron.schedule( + retrySchedule, + async () => { + console.log("Running failed payout retry process..."); + + try { + const results = await PayoutService.retryFailedPayouts(); + + if (results.totalProcessed > 0) { + console.log( + `Retry batch completed: ${results.successful.length} successful, ${results.failed.length} still failed` + ); + } + } catch (error) { + console.error("Error in retry payout processing:", error); + } + }, + { + scheduled: false, + timezone: "America/New_York", + } + ); + + // Start the jobs + payoutJob.start(); + retryJob.start(); + + console.log("Payout processor jobs scheduled:"); + console.log("- Hourly payout processing: " + paymentsSchedule); + console.log("- Daily retry processing: " + retrySchedule); + + return { + payoutJob, + retryJob, + + stop() { + payoutJob.stop(); + retryJob.stop(); + console.log("Payout processor jobs stopped"); + }, + + getStatus() { + return { + payoutJobRunning: payoutJob.getStatus() === "scheduled", + retryJobRunning: retryJob.getStatus() === "scheduled", + }; + }, + }; + } +} + +module.exports = PayoutProcessor; diff --git a/backend/models/Rental.js b/backend/models/Rental.js index daee2c4..505862c 100644 --- a/backend/models/Rental.js +++ b/backend/models/Rental.js @@ -49,6 +49,22 @@ 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 + }, status: { type: DataTypes.ENUM('pending', 'confirmed', 'active', 'completed', 'cancelled'), defaultValue: 'pending' @@ -57,6 +73,16 @@ const Rental = sequelize.define('Rental', { type: DataTypes.ENUM('pending', 'paid', 'refunded'), defaultValue: 'pending' }, + payoutStatus: { + type: DataTypes.ENUM('pending', 'processing', 'completed', 'failed'), + defaultValue: 'pending' + }, + payoutProcessedAt: { + type: DataTypes.DATE + }, + stripeTransferId: { + type: DataTypes.STRING + }, deliveryMethod: { type: DataTypes.ENUM('pickup', 'delivery'), defaultValue: 'pickup' diff --git a/backend/models/User.js b/backend/models/User.js index 248ac72..ae36aa0 100644 --- a/backend/models/User.js +++ b/backend/models/User.js @@ -98,6 +98,10 @@ const User = sequelize.define('User', { friday: { availableAfter: "09:00", availableBefore: "17:00" }, saturday: { availableAfter: "09:00", availableBefore: "17:00" } } + }, + stripeConnectedAccountId: { + type: DataTypes.STRING, + allowNull: true } }, { hooks: { diff --git a/backend/package-lock.json b/backend/package-lock.json index 3ddfe7e..22c28ef 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -16,6 +16,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "node-cron": "^3.0.3", "pg": "^8.16.3", "sequelize": "^6.37.7", "sequelize-cli": "^6.6.3", @@ -1372,6 +1373,27 @@ "node": ">= 0.6" } }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/nodemon": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", diff --git a/backend/package.json b/backend/package.json index 41403c5..4a64807 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "node-cron": "^3.0.3", "pg": "^8.16.3", "sequelize": "^6.37.7", "sequelize-cli": "^6.6.3", diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js index 8e9fda3..c67aa0a 100644 --- a/backend/routes/rentals.js +++ b/backend/routes/rentals.js @@ -2,6 +2,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 FeeCalculator = require("../utils/feeCalculator"); const router = express.Router(); // Helper function to check and update review visibility @@ -139,7 +140,10 @@ router.post("/", authenticateToken, async (req, res) => { const rentalDays = Math.ceil( (new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24) ); - const totalAmount = rentalDays * (item.pricePerDay || 0); + const baseRentalAmount = rentalDays * (item.pricePerDay || 0); + + // Calculate fees using FeeCalculator + const fees = FeeCalculator.calculateRentalFees(baseRentalAmount); const rental = await Rental.create({ itemId, @@ -149,7 +153,11 @@ router.post("/", authenticateToken, async (req, res) => { endDate, startTime, endTime, - totalAmount, + totalAmount: fees.totalChargedAmount, + baseRentalAmount: fees.baseRentalAmount, + platformFee: fees.platformFee, + processingFee: fees.processingFee, + payoutAmount: fees.payoutAmount, deliveryMethod, deliveryAddress, notes, @@ -376,4 +384,54 @@ router.post("/:id/review", authenticateToken, async (req, res) => { } }); +// Calculate fees for rental pricing display +router.post("/calculate-fees", authenticateToken, async (req, res) => { + try { + const { baseAmount } = req.body; + + if (!baseAmount || baseAmount <= 0) { + return res.status(400).json({ error: "Valid base amount is required" }); + } + + const fees = FeeCalculator.calculateRentalFees(baseAmount); + const displayFees = FeeCalculator.formatFeesForDisplay(fees); + + res.json({ + fees, + display: displayFees, + }); + } catch (error) { + console.error("Error calculating fees:", error); + res.status(500).json({ error: error.message }); + } +}); + +// Get payout status for owner's rentals +router.get("/payouts/status", authenticateToken, async (req, res) => { + try { + const ownerRentals = await Rental.findAll({ + where: { + ownerId: req.user.id, + status: "completed", + }, + attributes: [ + "id", + "baseRentalAmount", + "platformFee", + "payoutAmount", + "payoutStatus", + "payoutProcessedAt", + "stripeTransferId", + ], + include: [{ model: Item, as: "item", attributes: ["name"] }], + order: [["createdAt", "DESC"]], + }); + + res.json(ownerRentals); + } catch (error) { + console.error("Error getting payout status:", error); + res.status(500).json({ error: error.message }); + } +}); + module.exports = router; diff --git a/backend/routes/stripe.js b/backend/routes/stripe.js index a98b700..b65c96d 100644 --- a/backend/routes/stripe.js +++ b/backend/routes/stripe.js @@ -66,156 +66,100 @@ router.get("/checkout-session/:sessionId", async (req, res) => { } }); -// // Create connected account -// router.post("/accounts", authenticateToken, async (req, res) => { -// try { -// const user = await User.findByPk(req.user.id); +// 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" }); -// } + 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" }); -// } + // 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 -// }); + // 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, -// }); + // 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 }); -// } -// }); + 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); +// 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" }); -// } + if (!user || !user.stripeConnectedAccountId) { + return res.status(400).json({ error: "No connected account found" }); + } -// const { refreshUrl, returnUrl } = req.body; + const { refreshUrl, returnUrl } = req.body; -// if (!refreshUrl || !returnUrl) { -// return res -// .status(400) -// .json({ error: "refreshUrl and returnUrl are required" }); -// } + if (!refreshUrl || !returnUrl) { + return res + .status(400) + .json({ error: "refreshUrl and returnUrl are required" }); + } -// const accountLink = await StripeService.createAccountLink( -// user.stripeConnectedAccountId, -// refreshUrl, -// returnUrl -// ); + 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 }); -// } -// }); + 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); +// 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" }); -// } + if (!user || !user.stripeConnectedAccountId) { + return res.status(400).json({ error: "No connected account found" }); + } -// const accountStatus = await StripeService.getAccountStatus( -// user.stripeConnectedAccountId -// ); + 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 }); -// } -// }); + 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 }); + } +}); module.exports = router; diff --git a/backend/server.js b/backend/server.js index 914d09c..ef42a3f 100644 --- a/backend/server.js +++ b/backend/server.js @@ -21,6 +21,8 @@ const betaRoutes = require("./routes/beta"); const itemRequestRoutes = require("./routes/itemRequests"); const stripeRoutes = require("./routes/stripe"); +const PayoutProcessor = require("./jobs/payoutProcessor"); + const app = express(); app.use(cors()); @@ -52,6 +54,10 @@ sequelize .sync({ alter: true }) .then(() => { console.log("Database synced"); + + // Start the payout processor + const payoutJobs = PayoutProcessor.startScheduledPayouts(); + app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); diff --git a/backend/services/payoutService.js b/backend/services/payoutService.js new file mode 100644 index 0000000..1eb2e59 --- /dev/null +++ b/backend/services/payoutService.js @@ -0,0 +1,194 @@ +const { Rental, User } = require("../models"); +const StripeService = require("./stripeService"); +const { Op } = require("sequelize"); + +class PayoutService { + static async getEligiblePayouts() { + try { + const eligibleRentals = await Rental.findAll({ + where: { + status: "completed", + paymentStatus: "paid", + payoutStatus: "pending", + }, + include: [ + { + model: User, + as: "owner", + where: { + stripeConnectedAccountId: { + [Op.not]: null, + }, + }, + }, + ], + }); + + return eligibleRentals; + } catch (error) { + console.error("Error getting eligible payouts:", error); + throw error; + } + } + + static async processRentalPayout(rental) { + try { + if (!rental.owner || !rental.owner.stripeConnectedAccountId) { + throw new Error("Owner does not have a connected Stripe account"); + } + + if (rental.payoutStatus !== "pending") { + throw new Error("Rental payout has already been processed"); + } + + if (!rental.payoutAmount || rental.payoutAmount <= 0) { + throw new Error("Invalid payout amount"); + } + + // Update status to processing + await rental.update({ + payoutStatus: "processing", + }); + + // Create Stripe transfer + const transfer = await StripeService.createTransfer({ + amount: rental.payoutAmount, + destination: rental.owner.stripeConnectedAccountId, + metadata: { + rentalId: rental.id, + ownerId: rental.ownerId, + baseAmount: rental.baseRentalAmount.toString(), + platformFee: rental.platformFee.toString(), + }, + }); + + // Update rental with successful payout + await rental.update({ + payoutStatus: "completed", + payoutProcessedAt: new Date(), + stripeTransferId: transfer.id, + }); + + console.log( + `Payout completed for rental ${rental.id}: $${rental.payoutAmount} to ${rental.owner.stripeConnectedAccountId}` + ); + + return { + success: true, + transferId: transfer.id, + amount: rental.payoutAmount, + }; + } catch (error) { + console.error(`Error processing payout for rental ${rental.id}:`, error); + + // Update status to failed + await rental.update({ + payoutStatus: "failed", + }); + + throw error; + } + } + + static async processAllEligiblePayouts() { + try { + const eligibleRentals = await this.getEligiblePayouts(); + + console.log( + `Found ${eligibleRentals.length} eligible rentals for payout` + ); + + const results = { + successful: [], + failed: [], + totalProcessed: eligibleRentals.length, + }; + + for (const rental of eligibleRentals) { + try { + const result = await this.processRentalPayout(rental); + results.successful.push({ + rentalId: rental.id, + amount: result.amount, + transferId: result.transferId, + }); + } catch (error) { + results.failed.push({ + rentalId: rental.id, + error: error.message, + }); + } + } + + console.log( + `Payout processing complete: ${results.successful.length} successful, ${results.failed.length} failed` + ); + + return results; + } catch (error) { + console.error("Error processing all eligible payouts:", error); + throw error; + } + } + + static async retryFailedPayouts() { + try { + const failedRentals = await Rental.findAll({ + where: { + status: "completed", + paymentStatus: "paid", + payoutStatus: "failed", + }, + include: [ + { + model: User, + as: "owner", + where: { + stripeConnectedAccountId: { + [Op.not]: null, + }, + }, + }, + ], + }); + + console.log(`Found ${failedRentals.length} failed payouts to retry`); + + const results = { + successful: [], + failed: [], + totalProcessed: failedRentals.length, + }; + + for (const rental of failedRentals) { + try { + // Reset to pending before retrying + await rental.update({ payoutStatus: "pending" }); + + const result = await this.processRentalPayout(rental); + results.successful.push({ + rentalId: rental.id, + amount: result.amount, + transferId: result.transferId, + }); + } catch (error) { + results.failed.push({ + rentalId: rental.id, + error: error.message, + }); + } + } + + console.log( + `Retry processing complete: ${results.successful.length} successful, ${results.failed.length} failed` + ); + + return results; + } catch (error) { + console.error("Error retrying failed payouts:", error); + throw error; + } + } +} + +module.exports = PayoutService; diff --git a/backend/services/stripeService.js b/backend/services/stripeService.js index 3e91523..966d3cd 100644 --- a/backend/services/stripeService.js +++ b/backend/services/stripeService.js @@ -45,113 +45,76 @@ class StripeService { } } - // static async createConnectedAccount({ email, country = "US" }) { - // try { - // const account = await stripe.accounts.create({ - // type: "standard", - // email, - // country, - // controller: { - // stripe_dashboard: { - // type: "full", - // }, - // }, - // capabilities: { - // transfers: { requested: true }, - // }, - // }); + static async createConnectedAccount({ email, country = "US" }) { + try { + const account = await stripe.accounts.create({ + type: "standard", + email, + country, + capabilities: { + transfers: { requested: true }, + }, + }); - // return account; - // } catch (error) { - // console.error("Error creating connected account:", error); - // throw error; - // } - // } + return account; + } catch (error) { + console.error("Error creating connected account:", error); + throw error; + } + } - // static async createAccountLink(accountId, refreshUrl, returnUrl) { - // try { - // const accountLink = await stripe.accountLinks.create({ - // account: accountId, - // refresh_url: refreshUrl, - // return_url: returnUrl, - // type: "account_onboarding", - // }); + static async createAccountLink(accountId, refreshUrl, returnUrl) { + try { + const accountLink = await stripe.accountLinks.create({ + account: accountId, + refresh_url: refreshUrl, + return_url: returnUrl, + type: "account_onboarding", + }); - // return accountLink; - // } catch (error) { - // console.error("Error creating account link:", error); - // throw error; - // } - // } + return accountLink; + } catch (error) { + console.error("Error creating account link:", error); + throw error; + } + } - // static async getAccountStatus(accountId) { - // try { - // const account = await stripe.accounts.retrieve(accountId); - // return { - // id: account.id, - // details_submitted: account.details_submitted, - // payouts_enabled: account.payouts_enabled, - // capabilities: account.capabilities, - // requirements: account.requirements, - // }; - // } catch (error) { - // console.error("Error retrieving account status:", error); - // throw error; - // } - // } + static async getAccountStatus(accountId) { + try { + const account = await stripe.accounts.retrieve(accountId); + return { + id: account.id, + details_submitted: account.details_submitted, + payouts_enabled: account.payouts_enabled, + capabilities: account.capabilities, + requirements: account.requirements, + }; + } catch (error) { + console.error("Error retrieving account status:", error); + throw error; + } + } - // static async createPaymentIntent({ - // amount, - // currency = "usd", - // connectedAccountId, - // applicationFeeAmount, - // metadata = {}, - // }) { - // try { - // const paymentIntent = await stripe.paymentIntents.create({ - // amount, - // currency, - // transfer_data: { - // destination: connectedAccountId, - // }, - // application_fee_amount: applicationFeeAmount, - // metadata, - // automatic_payment_methods: { - // enabled: true, - // }, - // }); + static async createTransfer({ + amount, + currency = "usd", + destination, + metadata = {}, + }) { + try { + const transfer = await stripe.transfers.create({ + amount: Math.round(amount * 100), // Convert to cents + currency, + destination, + metadata, + }); - // return paymentIntent; - // } catch (error) { - // console.error("Error creating payment intent:", error); - // throw error; - // } - // } - - // static async confirmPaymentIntent(paymentIntentId, paymentMethodId) { - // try { - // const paymentIntent = await stripe.paymentIntents.confirm( - // paymentIntentId, - // { - // payment_method: paymentMethodId, - // } - // ); - - // return paymentIntent; - // } catch (error) { - // console.error("Error confirming payment intent:", error); - // throw error; - // } - // } - - // static async retrievePaymentIntent(paymentIntentId) { - // try { - // return await stripe.paymentIntents.retrieve(paymentIntentId); - // } catch (error) { - // console.error("Error retrieving payment intent:", error); - // throw error; - // } - // } + return transfer; + } catch (error) { + console.error("Error creating transfer:", error); + throw error; + } + } } module.exports = StripeService; diff --git a/backend/utils/feeCalculator.js b/backend/utils/feeCalculator.js new file mode 100644 index 0000000..9c0e992 --- /dev/null +++ b/backend/utils/feeCalculator.js @@ -0,0 +1,30 @@ +class FeeCalculator { + static calculateRentalFees(baseAmount) { + const platformFeeRate = 0.2; + const stripeRate = 0.029; + const stripeFixedFee = 0.3; + + const platformFee = baseAmount * platformFeeRate; + const processingFee = baseAmount * stripeRate + stripeFixedFee; + + return { + baseRentalAmount: parseFloat(baseAmount.toFixed(2)), + platformFee: parseFloat(platformFee.toFixed(2)), + processingFee: parseFloat(processingFee.toFixed(2)), + totalChargedAmount: parseFloat((baseAmount + processingFee).toFixed(2)), + payoutAmount: parseFloat((baseAmount - platformFee).toFixed(2)), + }; + } + + static formatFeesForDisplay(fees) { + return { + baseRental: `$${fees.baseRentalAmount.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)}`, + }; + } +} + +module.exports = FeeCalculator; diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx index 6f6195f..24197df 100644 --- a/frontend/src/pages/Profile.tsx +++ b/frontend/src/pages/Profile.tsx @@ -1402,19 +1402,6 @@ const Profile: React.FC = () => { )} - - {activeSection === "payment" && ( -
-

Payment Methods

-
-
-

- Payment method management coming soon... -

-
-
-
- )}