started payouts

This commit is contained in:
jackiettran
2025-08-29 00:32:06 -04:00
parent 0f04182768
commit b52104c3fa
13 changed files with 578 additions and 252 deletions

1
.gitignore vendored
View File

@@ -17,6 +17,7 @@ node_modules/
.env.development.local .env.development.local
.env.test.local .env.test.local
.env.production.local .env.production.local
.mcp.json
# Logs # Logs
npm-debug.log* npm-debug.log*

View File

@@ -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;

View File

@@ -49,6 +49,22 @@ const Rental = sequelize.define('Rental', {
type: DataTypes.DECIMAL(10, 2), type: DataTypes.DECIMAL(10, 2),
allowNull: false 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: { status: {
type: DataTypes.ENUM('pending', 'confirmed', 'active', 'completed', 'cancelled'), type: DataTypes.ENUM('pending', 'confirmed', 'active', 'completed', 'cancelled'),
defaultValue: 'pending' defaultValue: 'pending'
@@ -57,6 +73,16 @@ const Rental = sequelize.define('Rental', {
type: DataTypes.ENUM('pending', 'paid', 'refunded'), type: DataTypes.ENUM('pending', 'paid', 'refunded'),
defaultValue: 'pending' defaultValue: 'pending'
}, },
payoutStatus: {
type: DataTypes.ENUM('pending', 'processing', 'completed', 'failed'),
defaultValue: 'pending'
},
payoutProcessedAt: {
type: DataTypes.DATE
},
stripeTransferId: {
type: DataTypes.STRING
},
deliveryMethod: { deliveryMethod: {
type: DataTypes.ENUM('pickup', 'delivery'), type: DataTypes.ENUM('pickup', 'delivery'),
defaultValue: 'pickup' defaultValue: 'pickup'

View File

@@ -98,6 +98,10 @@ const User = sequelize.define('User', {
friday: { availableAfter: "09:00", availableBefore: "17:00" }, friday: { availableAfter: "09:00", availableBefore: "17:00" },
saturday: { availableAfter: "09:00", availableBefore: "17:00" } saturday: { availableAfter: "09:00", availableBefore: "17:00" }
} }
},
stripeConnectedAccountId: {
type: DataTypes.STRING,
allowNull: true
} }
}, { }, {
hooks: { hooks: {

View File

@@ -16,6 +16,7 @@
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^2.0.2", "multer": "^2.0.2",
"node-cron": "^3.0.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3", "sequelize-cli": "^6.6.3",
@@ -1372,6 +1373,27 @@
"node": ">= 0.6" "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": { "node_modules/nodemon": {
"version": "3.1.10", "version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",

View File

@@ -23,6 +23,7 @@
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^2.0.2", "multer": "^2.0.2",
"node-cron": "^3.0.3",
"pg": "^8.16.3", "pg": "^8.16.3",
"sequelize": "^6.37.7", "sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3", "sequelize-cli": "^6.6.3",

View File

@@ -2,6 +2,7 @@ const express = require("express");
const { Op } = require("sequelize"); const { Op } = require("sequelize");
const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations const { Rental, Item, User } = require("../models"); // Import from models/index.js to get models with associations
const { authenticateToken } = require("../middleware/auth"); const { authenticateToken } = require("../middleware/auth");
const FeeCalculator = require("../utils/feeCalculator");
const router = express.Router(); const router = express.Router();
// Helper function to check and update review visibility // Helper function to check and update review visibility
@@ -139,7 +140,10 @@ router.post("/", authenticateToken, async (req, res) => {
const rentalDays = Math.ceil( const rentalDays = Math.ceil(
(new Date(endDate) - new Date(startDate)) / (1000 * 60 * 60 * 24) (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({ const rental = await Rental.create({
itemId, itemId,
@@ -149,7 +153,11 @@ router.post("/", authenticateToken, async (req, res) => {
endDate, endDate,
startTime, startTime,
endTime, endTime,
totalAmount, totalAmount: fees.totalChargedAmount,
baseRentalAmount: fees.baseRentalAmount,
platformFee: fees.platformFee,
processingFee: fees.processingFee,
payoutAmount: fees.payoutAmount,
deliveryMethod, deliveryMethod,
deliveryAddress, deliveryAddress,
notes, 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; module.exports = router;

View File

@@ -66,156 +66,100 @@ router.get("/checkout-session/:sessionId", async (req, res) => {
} }
}); });
// // Create connected account // Create connected account
// router.post("/accounts", authenticateToken, async (req, res) => { router.post("/accounts", authenticateToken, async (req, res) => {
// try { try {
// const user = await User.findByPk(req.user.id); const user = await User.findByPk(req.user.id);
// if (!user) { if (!user) {
// return res.status(404).json({ error: "User not found" }); return res.status(404).json({ error: "User not found" });
// } }
// // Check if user already has a connected account // Check if user already has a connected account
// if (user.stripeConnectedAccountId) { if (user.stripeConnectedAccountId) {
// return res return res
// .status(400) .status(400)
// .json({ error: "User already has a connected account" }); .json({ error: "User already has a connected account" });
// } }
// // Create connected account // Create connected account
// const account = await StripeService.createConnectedAccount({ const account = await StripeService.createConnectedAccount({
// email: user.email, email: user.email,
// country: "US", // You may want to make this configurable country: "US", // You may want to make this configurable
// }); });
// // Update user with account ID // Update user with account ID
// await user.update({ await user.update({
// stripeConnectedAccountId: account.id, stripeConnectedAccountId: account.id,
// }); });
// res.json({ res.json({
// stripeConnectedAccountId: account.id, stripeConnectedAccountId: account.id,
// success: true, success: true,
// }); });
// } catch (error) { } catch (error) {
// console.error("Error creating connected account:", error); console.error("Error creating connected account:", error);
// res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
// } }
// }); });
// // Generate onboarding link // Generate onboarding link
// router.post("/account-links", authenticateToken, async (req, res) => { router.post("/account-links", authenticateToken, async (req, res) => {
// try { try {
// const user = await User.findByPk(req.user.id); const user = await User.findByPk(req.user.id);
// if (!user || !user.stripeConnectedAccountId) { if (!user || !user.stripeConnectedAccountId) {
// return res.status(400).json({ error: "No connected account found" }); return res.status(400).json({ error: "No connected account found" });
// } }
// const { refreshUrl, returnUrl } = req.body; const { refreshUrl, returnUrl } = req.body;
// if (!refreshUrl || !returnUrl) { if (!refreshUrl || !returnUrl) {
// return res return res
// .status(400) .status(400)
// .json({ error: "refreshUrl and returnUrl are required" }); .json({ error: "refreshUrl and returnUrl are required" });
// } }
// const accountLink = await StripeService.createAccountLink( const accountLink = await StripeService.createAccountLink(
// user.stripeConnectedAccountId, user.stripeConnectedAccountId,
// refreshUrl, refreshUrl,
// returnUrl returnUrl
// ); );
// res.json({ res.json({
// url: accountLink.url, url: accountLink.url,
// expiresAt: accountLink.expires_at, expiresAt: accountLink.expires_at,
// }); });
// } catch (error) { } catch (error) {
// console.error("Error creating account link:", error); console.error("Error creating account link:", error);
// res.status(500).json({ error: error.message }); res.status(500).json({ error: error.message });
// } }
// }); });
// // Get account status // Get account status
// router.get("/account-status", authenticateToken, async (req, res) => { router.get("/account-status", authenticateToken, async (req, res) => {
// try { try {
// const user = await User.findByPk(req.user.id); const user = await User.findByPk(req.user.id);
// if (!user || !user.stripeConnectedAccountId) { if (!user || !user.stripeConnectedAccountId) {
// return res.status(400).json({ error: "No connected account found" }); return res.status(400).json({ error: "No connected account found" });
// } }
// const accountStatus = await StripeService.getAccountStatus( const accountStatus = await StripeService.getAccountStatus(
// user.stripeConnectedAccountId user.stripeConnectedAccountId
// ); );
// res.json({ res.json({
// accountId: accountStatus.id, accountId: accountStatus.id,
// detailsSubmitted: accountStatus.details_submitted, detailsSubmitted: accountStatus.details_submitted,
// payoutsEnabled: accountStatus.payouts_enabled, payoutsEnabled: accountStatus.payouts_enabled,
// capabilities: accountStatus.capabilities, capabilities: accountStatus.capabilities,
// requirements: accountStatus.requirements, requirements: accountStatus.requirements,
// }); });
// } catch (error) { } catch (error) {
// console.error("Error getting account status:", error); console.error("Error getting account status:", error);
// res.status(500).json({ error: error.message }); 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; module.exports = router;

View File

@@ -21,6 +21,8 @@ const betaRoutes = require("./routes/beta");
const itemRequestRoutes = require("./routes/itemRequests"); const itemRequestRoutes = require("./routes/itemRequests");
const stripeRoutes = require("./routes/stripe"); const stripeRoutes = require("./routes/stripe");
const PayoutProcessor = require("./jobs/payoutProcessor");
const app = express(); const app = express();
app.use(cors()); app.use(cors());
@@ -52,6 +54,10 @@ sequelize
.sync({ alter: true }) .sync({ alter: true })
.then(() => { .then(() => {
console.log("Database synced"); console.log("Database synced");
// Start the payout processor
const payoutJobs = PayoutProcessor.startScheduledPayouts();
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`); console.log(`Server is running on port ${PORT}`);
}); });

View File

@@ -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;

View File

@@ -45,113 +45,76 @@ class StripeService {
} }
} }
// static async createConnectedAccount({ email, country = "US" }) { static async createConnectedAccount({ email, country = "US" }) {
// try { try {
// const account = await stripe.accounts.create({ const account = await stripe.accounts.create({
// type: "standard", type: "standard",
// email, email,
// country, country,
// controller: { capabilities: {
// stripe_dashboard: { transfers: { requested: true },
// type: "full", },
// }, });
// },
// capabilities: {
// transfers: { requested: true },
// },
// });
// return account; return account;
// } catch (error) { } catch (error) {
// console.error("Error creating connected account:", error); console.error("Error creating connected account:", error);
// throw error; throw error;
// } }
// } }
// static async createAccountLink(accountId, refreshUrl, returnUrl) { static async createAccountLink(accountId, refreshUrl, returnUrl) {
// try { try {
// const accountLink = await stripe.accountLinks.create({ const accountLink = await stripe.accountLinks.create({
// account: accountId, account: accountId,
// refresh_url: refreshUrl, refresh_url: refreshUrl,
// return_url: returnUrl, return_url: returnUrl,
// type: "account_onboarding", type: "account_onboarding",
// }); });
// return accountLink; return accountLink;
// } catch (error) { } catch (error) {
// console.error("Error creating account link:", error); console.error("Error creating account link:", error);
// throw error; throw error;
// } }
// } }
// static async getAccountStatus(accountId) { static async getAccountStatus(accountId) {
// try { try {
// const account = await stripe.accounts.retrieve(accountId); const account = await stripe.accounts.retrieve(accountId);
// return { return {
// id: account.id, id: account.id,
// details_submitted: account.details_submitted, details_submitted: account.details_submitted,
// payouts_enabled: account.payouts_enabled, payouts_enabled: account.payouts_enabled,
// capabilities: account.capabilities, capabilities: account.capabilities,
// requirements: account.requirements, requirements: account.requirements,
// }; };
// } catch (error) { } catch (error) {
// console.error("Error retrieving account status:", error); console.error("Error retrieving account status:", error);
// throw error; throw error;
// } }
// } }
// static async createPaymentIntent({ static async createTransfer({
// amount, amount,
// currency = "usd", currency = "usd",
// connectedAccountId, destination,
// applicationFeeAmount, metadata = {},
// metadata = {}, }) {
// }) { try {
// try { const transfer = await stripe.transfers.create({
// const paymentIntent = await stripe.paymentIntents.create({ amount: Math.round(amount * 100), // Convert to cents
// amount, currency,
// currency, destination,
// transfer_data: { metadata,
// destination: connectedAccountId, });
// },
// application_fee_amount: applicationFeeAmount,
// metadata,
// automatic_payment_methods: {
// enabled: true,
// },
// });
// return paymentIntent; return transfer;
// } catch (error) { } catch (error) {
// console.error("Error creating payment intent:", error); console.error("Error creating transfer:", error);
// throw 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;
// }
// }
} }
module.exports = StripeService; module.exports = StripeService;

View File

@@ -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;

View File

@@ -1402,19 +1402,6 @@ const Profile: React.FC = () => {
</div> </div>
</div> </div>
)} )}
{activeSection === "payment" && (
<div>
<h4 className="mb-4">Payment Methods</h4>
<div className="card">
<div className="card-body">
<p className="text-muted">
Payment method management coming soon...
</p>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>