From 8de814fdee5595c28d69465867de332c3f8007fd Mon Sep 17 00:00:00 2001 From: jackiettran <41605212+jackiettran@users.noreply.github.com> Date: Tue, 25 Nov 2025 16:40:42 -0500 Subject: [PATCH] replaced vague notes with specific intended use, also fixed modal on top of modal for reviews --- .../20241124000005-create-rentals.js | 210 ++++++++++++++++++ backend/models/Rental.js | 2 +- backend/routes/rentals.js | 15 +- .../services/email/core/TemplateManager.js | 2 +- .../email/domain/RentalFlowEmailService.js | 3 +- backend/services/lateReturnService.js | 7 +- .../emails/rentalRequestToOwner.html | 4 +- backend/tests/unit/routes/rentals.test.js | 1 - .../tests/unit/services/emailService.test.js | 36 --- frontend/src/components/ReturnStatusModal.tsx | 11 +- frontend/src/components/ReviewModal.tsx | 19 +- frontend/src/components/ReviewRenterModal.tsx | 19 +- frontend/src/pages/Owning.tsx | 12 +- frontend/src/pages/RentItem.tsx | 22 ++ frontend/src/services/api.ts | 2 +- frontend/src/types/index.ts | 2 +- 16 files changed, 282 insertions(+), 85 deletions(-) create mode 100644 backend/migrations/20241124000005-create-rentals.js diff --git a/backend/migrations/20241124000005-create-rentals.js b/backend/migrations/20241124000005-create-rentals.js new file mode 100644 index 0000000..f8aae32 --- /dev/null +++ b/backend/migrations/20241124000005-create-rentals.js @@ -0,0 +1,210 @@ +"use strict"; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.createTable("Rentals", { + id: { + type: Sequelize.UUID, + defaultValue: Sequelize.UUIDV4, + primaryKey: true, + }, + itemId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: "Items", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + renterId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: "Users", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + ownerId: { + type: Sequelize.UUID, + allowNull: false, + references: { + model: "Users", + key: "id", + }, + onUpdate: "CASCADE", + onDelete: "CASCADE", + }, + startDateTime: { + type: Sequelize.DATE, + allowNull: false, + }, + endDateTime: { + type: Sequelize.DATE, + allowNull: false, + }, + totalAmount: { + type: Sequelize.DECIMAL(10, 2), + allowNull: false, + }, + platformFee: { + type: Sequelize.DECIMAL(10, 2), + allowNull: false, + }, + payoutAmount: { + type: Sequelize.DECIMAL(10, 2), + allowNull: false, + }, + status: { + type: Sequelize.ENUM( + "pending", + "confirmed", + "declined", + "active", + "completed", + "cancelled", + "returned_late", + "returned_late_and_damaged", + "damaged", + "lost" + ), + allowNull: false, + }, + paymentStatus: { + type: Sequelize.ENUM("pending", "paid", "refunded", "not_required"), + allowNull: false, + }, + payoutStatus: { + type: Sequelize.ENUM("pending", "completed", "failed"), + allowNull: true, + }, + payoutProcessedAt: { + type: Sequelize.DATE, + }, + stripeTransferId: { + type: Sequelize.STRING, + }, + refundAmount: { + type: Sequelize.DECIMAL(10, 2), + }, + refundProcessedAt: { + type: Sequelize.DATE, + }, + refundReason: { + type: Sequelize.TEXT, + }, + stripeRefundId: { + type: Sequelize.STRING, + }, + cancelledBy: { + type: Sequelize.ENUM("renter", "owner"), + }, + cancelledAt: { + type: Sequelize.DATE, + }, + declineReason: { + type: Sequelize.TEXT, + }, + stripePaymentMethodId: { + type: Sequelize.STRING, + }, + stripePaymentIntentId: { + type: Sequelize.STRING, + }, + paymentMethodBrand: { + type: Sequelize.STRING, + }, + paymentMethodLast4: { + type: Sequelize.STRING, + }, + chargedAt: { + type: Sequelize.DATE, + }, + deliveryMethod: { + type: Sequelize.ENUM("pickup", "delivery"), + defaultValue: "pickup", + }, + deliveryAddress: { + type: Sequelize.TEXT, + }, + intendedUse: { + type: Sequelize.TEXT, + }, + itemRating: { + type: Sequelize.INTEGER, + }, + itemReview: { + type: Sequelize.TEXT, + }, + itemReviewSubmittedAt: { + type: Sequelize.DATE, + }, + itemReviewVisible: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, + renterRating: { + type: Sequelize.INTEGER, + }, + renterReview: { + type: Sequelize.TEXT, + }, + renterReviewSubmittedAt: { + type: Sequelize.DATE, + }, + renterReviewVisible: { + type: Sequelize.BOOLEAN, + defaultValue: false, + }, + itemPrivateMessage: { + type: Sequelize.TEXT, + }, + renterPrivateMessage: { + type: Sequelize.TEXT, + }, + actualReturnDateTime: { + type: Sequelize.DATE, + }, + lateFees: { + type: Sequelize.DECIMAL(10, 2), + defaultValue: 0.0, + }, + damageFees: { + type: Sequelize.DECIMAL(10, 2), + defaultValue: 0.0, + }, + replacementFees: { + type: Sequelize.DECIMAL(10, 2), + defaultValue: 0.0, + }, + itemLostReportedAt: { + type: Sequelize.DATE, + }, + damageAssessment: { + type: Sequelize.JSONB, + defaultValue: {}, + }, + createdAt: { + type: Sequelize.DATE, + allowNull: false, + }, + updatedAt: { + type: Sequelize.DATE, + allowNull: false, + }, + }); + + // Add indexes + await queryInterface.addIndex("Rentals", ["itemId"]); + await queryInterface.addIndex("Rentals", ["renterId"]); + await queryInterface.addIndex("Rentals", ["ownerId"]); + await queryInterface.addIndex("Rentals", ["status"]); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.dropTable("Rentals"); + }, +}; diff --git a/backend/models/Rental.js b/backend/models/Rental.js index fc4bb9b..26f9aa3 100644 --- a/backend/models/Rental.js +++ b/backend/models/Rental.js @@ -124,7 +124,7 @@ const Rental = sequelize.define("Rental", { deliveryAddress: { type: DataTypes.TEXT, }, - notes: { + intendedUse: { type: DataTypes.TEXT, }, // Renter's review of the item (existing fields renamed for clarity) diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js index a25d3f2..ace90c6 100644 --- a/backend/routes/rentals.js +++ b/backend/routes/rentals.js @@ -183,7 +183,7 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => { endDateTime, deliveryMethod, deliveryAddress, - notes, + intendedUse, stripePaymentMethodId, } = req.body; @@ -274,7 +274,7 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => { status: "pending", deliveryMethod, deliveryAddress, - notes, + intendedUse, }; // Only add stripePaymentMethodId if it's provided (for paid rentals) @@ -1099,7 +1099,7 @@ router.post("/:id/cancel", authenticateToken, async (req, res) => { // Mark item return status (owner only) router.post("/:id/mark-return", authenticateToken, async (req, res) => { try { - const { status, actualReturnDateTime, notes, statusOptions } = req.body; + const { status, actualReturnDateTime, statusOptions } = req.body; const rentalId = req.params.id; const userId = req.user.id; @@ -1133,7 +1133,6 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => { status: "completed", payoutStatus: "pending", actualReturnDateTime: actualReturnDateTime || rental.endDateTime, - notes: notes || null, }); // Fetch full rental details with associations for email @@ -1177,15 +1176,13 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => { status: "damaged", payoutStatus: "pending", actualReturnDateTime: actualReturnDateTime || rental.endDateTime, - notes: notes || null, }; // Check if ALSO returned late if (statusOptions?.returned_late && actualReturnDateTime) { const lateReturnDamaged = await LateReturnService.processLateReturn( rentalId, - actualReturnDateTime, - notes + actualReturnDateTime ); damageUpdates.status = "returned_late_and_damaged"; damageUpdates.lateFees = lateReturnDamaged.lateCalculation.lateFee; @@ -1207,8 +1204,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => { const lateReturn = await LateReturnService.processLateReturn( rentalId, - actualReturnDateTime, - notes + actualReturnDateTime ); updatedRental = lateReturn.rental; @@ -1221,7 +1217,6 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => { status: "lost", payoutStatus: "pending", itemLostReportedAt: new Date(), - notes: notes || null, }); // Send notification to customer service diff --git a/backend/services/email/core/TemplateManager.js b/backend/services/email/core/TemplateManager.js index bd852c2..66b49bc 100644 --- a/backend/services/email/core/TemplateManager.js +++ b/backend/services/email/core/TemplateManager.js @@ -303,7 +303,7 @@ class TemplateManager {

Total Amount: ${{totalAmount}}

Your Earnings: ${{payoutAmount}}

Delivery Method: {{deliveryMethod}}

-

Renter Notes: {{rentalNotes}}

+

Intended Use: {{intendedUse}}

Review & Respond

Please respond to this request within 24 hours.

` diff --git a/backend/services/email/domain/RentalFlowEmailService.js b/backend/services/email/domain/RentalFlowEmailService.js index c04c056..7ef053f 100644 --- a/backend/services/email/domain/RentalFlowEmailService.js +++ b/backend/services/email/domain/RentalFlowEmailService.js @@ -53,7 +53,6 @@ class RentalFlowEmailService { * @param {string} rental.totalAmount - Total rental amount * @param {string} rental.payoutAmount - Owner's payout amount * @param {string} rental.deliveryMethod - Delivery method - * @param {string} rental.notes - Rental notes from renter * @returns {Promise<{success: boolean, messageId?: string, error?: string}>} */ async sendRentalRequestEmail(owner, renter, rental) { @@ -88,7 +87,7 @@ class RentalFlowEmailService { ? parseFloat(rental.payoutAmount).toFixed(2) : "0.00", deliveryMethod: rental.deliveryMethod || "Not specified", - rentalNotes: rental.notes || "No additional notes provided", + intendedUse: rental.intendedUse || "Not specified", approveUrl: approveUrl, }; diff --git a/backend/services/lateReturnService.js b/backend/services/lateReturnService.js index 078305a..bb315fe 100644 --- a/backend/services/lateReturnService.js +++ b/backend/services/lateReturnService.js @@ -60,10 +60,9 @@ class LateReturnService { * Process late return and update rental with fees * @param {string} rentalId - Rental ID * @param {Date} actualReturnDateTime - When item was returned - * @param {string} notes - Optional notes about the return * @returns {Object} - Updated rental with late fee information */ - static async processLateReturn(rentalId, actualReturnDateTime, notes = null) { + static async processLateReturn(rentalId, actualReturnDateTime) { const rental = await Rental.findByPk(rentalId, { include: [{ model: Item, as: "item" }], }); @@ -84,10 +83,6 @@ class LateReturnService { payoutStatus: "pending", }; - if (notes) { - updates.notes = notes; - } - const updatedRental = await rental.update(updates); // Send notification to customer service if late return detected diff --git a/backend/templates/emails/rentalRequestToOwner.html b/backend/templates/emails/rentalRequestToOwner.html index 6c371cf..4607a6e 100644 --- a/backend/templates/emails/rentalRequestToOwner.html +++ b/backend/templates/emails/rentalRequestToOwner.html @@ -295,8 +295,8 @@
-

Renter's Notes:

-

{{rentalNotes}}

+

Intended Use:

+

{{intendedUse}}

diff --git a/backend/tests/unit/routes/rentals.test.js b/backend/tests/unit/routes/rentals.test.js index 0524234..927138b 100644 --- a/backend/tests/unit/routes/rentals.test.js +++ b/backend/tests/unit/routes/rentals.test.js @@ -289,7 +289,6 @@ describe('Rentals Routes', () => { endDateTime: '2024-01-15T18:00:00.000Z', deliveryMethod: 'pickup', deliveryAddress: null, - notes: 'Test rental', stripePaymentMethodId: 'pm_test123', }; diff --git a/backend/tests/unit/services/emailService.test.js b/backend/tests/unit/services/emailService.test.js index bac0594..a2e184b 100644 --- a/backend/tests/unit/services/emailService.test.js +++ b/backend/tests/unit/services/emailService.test.js @@ -449,7 +449,6 @@ describe('EmailService', () => { totalAmount: 150.00, payoutAmount: 135.00, deliveryMethod: 'pickup', - notes: 'Please have it ready by 9am', item: { name: 'Power Drill' } }; @@ -522,7 +521,6 @@ describe('EmailService', () => { totalAmount: 0, payoutAmount: 0, deliveryMethod: 'pickup', - notes: null, item: { name: 'Free Item' } }; @@ -531,39 +529,6 @@ describe('EmailService', () => { expect(result.success).toBe(true); }); - it('should handle missing rental notes', async () => { - const mockOwner = { - email: 'owner@example.com', - firstName: 'John' - }; - const mockRenter = { - firstName: 'Jane', - lastName: 'Doe' - }; - - User.findByPk - .mockResolvedValueOnce(mockOwner) - .mockResolvedValueOnce(mockRenter); - - const rental = { - id: 1, - ownerId: 10, - renterId: 20, - startDateTime: new Date('2024-12-01T10:00:00Z'), - endDateTime: new Date('2024-12-03T10:00:00Z'), - totalAmount: 100, - payoutAmount: 90, - deliveryMethod: 'delivery', - notes: null, // No notes - item: { name: 'Test Item' } - }; - - const result = await emailService.sendRentalRequestEmail(rental); - - expect(result.success).toBe(true); - expect(mockSend).toHaveBeenCalled(); - }); - it('should generate correct approval URL', async () => { const mockOwner = { email: 'owner@example.com', @@ -589,7 +554,6 @@ describe('EmailService', () => { totalAmount: 100, payoutAmount: 90, deliveryMethod: 'pickup', - notes: 'Test notes', item: { name: 'Test Item' } }; diff --git a/frontend/src/components/ReturnStatusModal.tsx b/frontend/src/components/ReturnStatusModal.tsx index 171bcea..f6c5717 100644 --- a/frontend/src/components/ReturnStatusModal.tsx +++ b/frontend/src/components/ReturnStatusModal.tsx @@ -24,7 +24,6 @@ const ReturnStatusModal: React.FC = ({ lost: false, }); const [actualReturnDateTime, setActualReturnDateTime] = useState(""); - const [notes, setNotes] = useState(""); const [conditionNotes, setConditionNotes] = useState(""); const [photos, setPhotos] = useState([]); const [processing, setProcessing] = useState(false); @@ -56,7 +55,6 @@ const ReturnStatusModal: React.FC = ({ lost: false, }); setActualReturnDateTime(""); - setNotes(""); setConditionNotes(""); setPhotos([]); setError(null); @@ -71,13 +69,13 @@ const ReturnStatusModal: React.FC = ({ // Create blob URLs for photo previews and clean them up const photoBlobUrls = useMemo(() => { - return photos.map(photo => URL.createObjectURL(photo)); + return photos.map((photo) => URL.createObjectURL(photo)); }, [photos]); // Cleanup blob URLs when photos change or component unmounts useEffect(() => { return () => { - photoBlobUrls.forEach(url => URL.revokeObjectURL(url)); + photoBlobUrls.forEach((url) => URL.revokeObjectURL(url)); }; }, [photoBlobUrls]); @@ -202,6 +200,7 @@ const ReturnStatusModal: React.FC = ({ ); if (!hasSelection) { setError("Please select at least one return status option"); + setProcessing(false); return; } @@ -316,7 +315,6 @@ const ReturnStatusModal: React.FC = ({ const data: any = { status: primaryStatus, statusOptions, // Send all selected options - notes: notes.trim() || undefined, }; if (statusOptions.returned_late) { @@ -346,7 +344,6 @@ const ReturnStatusModal: React.FC = ({ lost: false, }); setActualReturnDateTime(""); - setNotes(""); setConditionNotes(""); setPhotos([]); setError(null); @@ -950,7 +947,7 @@ const ReturnStatusModal: React.FC = ({ > Loading...
- Processing... + Submitting... ) : ( "Submit" diff --git a/frontend/src/components/ReviewModal.tsx b/frontend/src/components/ReviewModal.tsx index 8d11145..93094ff 100644 --- a/frontend/src/components/ReviewModal.tsx +++ b/frontend/src/components/ReviewModal.tsx @@ -69,6 +69,18 @@ const ReviewItemModal: React.FC = ({ if (!show) return null; + // Show success modal instead of review modal after successful submission + if (showSuccessModal) { + return ( + + ); + } + return (
= ({
- - ); }; diff --git a/frontend/src/components/ReviewRenterModal.tsx b/frontend/src/components/ReviewRenterModal.tsx index d57b9db..b927a23 100644 --- a/frontend/src/components/ReviewRenterModal.tsx +++ b/frontend/src/components/ReviewRenterModal.tsx @@ -69,6 +69,18 @@ const ReviewRenterModal: React.FC = ({ if (!show) return null; + // Show success modal instead of review modal after successful submission + if (showSuccessModal) { + return ( + + ); + } + return (
= ({
- - ); }; diff --git a/frontend/src/pages/Owning.tsx b/frontend/src/pages/Owning.tsx index ca30eb9..68cd8b0 100644 --- a/frontend/src/pages/Owning.tsx +++ b/frontend/src/pages/Owning.tsx @@ -359,6 +359,14 @@ const Owning: React.FC = () => { Total: ${rental.totalAmount}

+ {rental.intendedUse && rental.status === "pending" && ( +
+ Intended Use: +
+ {rental.intendedUse} +
+ )} + {rental.status === "cancelled" && rental.refundAmount !== undefined && (
@@ -620,9 +628,7 @@ const Owning: React.FC = () => { onClick={() => toggleAvailability(item)} className="btn btn-sm btn-outline-info" > - {item.isAvailable - ? "Mark Unavailable" - : "Mark Available"} + {item.isAvailable ? "Mark Unavailable" : "Mark Available"}