Compare commits

...

4 Commits

Author SHA1 Message Date
jackiettran
8e6af92cba schema updates to rental statuses 2025-11-24 18:08:12 -05:00
jackiettran
42a5412582 changed field from availability to isAvailable 2025-11-24 17:36:18 -05:00
jackiettran
bb16d659bd removed unneeded fields from item including needsTraining 2025-11-24 17:31:09 -05:00
jackiettran
34bbf06f0c no need for notes field for alpha invitation 2025-11-24 17:04:05 -05:00
19 changed files with 38 additions and 99 deletions

View File

@@ -46,10 +46,6 @@ const AlphaInvitation = sequelize.define(
defaultValue: "pending",
allowNull: false,
},
notes: {
type: DataTypes.TEXT,
allowNull: true,
},
},
{
indexes: [

View File

@@ -86,29 +86,13 @@ const Item = sequelize.define("Item", {
type: DataTypes.ARRAY(DataTypes.STRING),
defaultValue: [],
},
availability: {
isAvailable: {
type: DataTypes.BOOLEAN,
defaultValue: true,
},
specifications: {
type: DataTypes.JSONB,
defaultValue: {},
},
rules: {
type: DataTypes.TEXT,
},
minimumRentalDays: {
type: DataTypes.INTEGER,
defaultValue: 1,
},
maximumRentalDays: {
type: DataTypes.INTEGER,
},
needsTraining: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
availableAfter: {
type: DataTypes.STRING,
defaultValue: "09:00",

View File

@@ -64,15 +64,15 @@ const Rental = sequelize.define("Rental", {
"damaged",
"lost"
),
defaultValue: "pending",
allowNull: false,
},
paymentStatus: {
type: DataTypes.ENUM("pending", "paid", "refunded", "not_required"),
defaultValue: "pending",
allowNull: false,
},
payoutStatus: {
type: DataTypes.ENUM("pending", "processing", "completed", "failed"),
defaultValue: "pending",
type: DataTypes.ENUM("pending", "completed", "failed"),
allowNull: true,
},
payoutProcessedAt: {
type: DataTypes.DATE,

View File

@@ -98,7 +98,7 @@ router.get("/recommendations", authenticateToken, async (req, res) => {
// For now, just return random available items as recommendations
const recommendations = await Item.findAll({
where: {
availability: true,
isAvailable: true,
isDeleted: false,
},
limit: 10,

View File

@@ -192,7 +192,7 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
return res.status(404).json({ error: "Item not found" });
}
if (!item.availability) {
if (!item.isAvailable) {
return res.status(400).json({ error: "Item is not available" });
}
@@ -823,7 +823,7 @@ router.post("/:id/mark-completed", authenticateToken, async (req, res) => {
});
}
await rental.update({ status: "completed" });
await rental.update({ status: "completed", payoutStatus: "pending" });
const updatedRental = await Rental.findByPk(rental.id, {
include: [
@@ -1131,6 +1131,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
// Item returned on time
updatedRental = await rental.update({
status: "completed",
payoutStatus: "pending",
actualReturnDateTime: actualReturnDateTime || rental.endDateTime,
notes: notes || null,
});
@@ -1174,6 +1175,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
// Item returned damaged
const damageUpdates = {
status: "damaged",
payoutStatus: "pending",
actualReturnDateTime: actualReturnDateTime || rental.endDateTime,
notes: notes || null,
};
@@ -1217,6 +1219,7 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
// Item reported as lost
updatedRental = await rental.update({
status: "lost",
payoutStatus: "pending",
itemLostReportedAt: new Date(),
notes: notes || null,
});

View File

@@ -81,6 +81,7 @@ class LateReturnService {
const updates = {
actualReturnDateTime: new Date(actualReturnDateTime),
status: lateCalculation.isLate ? "returned_late" : "completed",
payoutStatus: "pending",
};
if (notes) {

View File

@@ -50,11 +50,6 @@ class PayoutService {
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,

View File

@@ -422,8 +422,8 @@ describe('Items Routes', () => {
describe('GET /recommendations', () => {
const mockRecommendations = [
{ id: 1, name: 'Item 1', availability: true },
{ id: 2, name: 'Item 2', availability: true }
{ id: 1, name: 'Item 1', isAvailable: true },
{ id: 2, name: 'Item 2', isAvailable: true }
];
it('should get recommendations for authenticated user', async () => {
@@ -443,7 +443,7 @@ describe('Items Routes', () => {
});
expect(mockItemFindAll).toHaveBeenCalledWith({
where: { availability: true },
where: { isAvailable: true, isDeleted: false },
limit: 10,
order: [['createdAt', 'DESC']]
});

View File

@@ -260,7 +260,7 @@ describe('Rentals Routes', () => {
id: 1,
name: 'Test Item',
ownerId: 2,
availability: true,
isAvailable: true,
pricePerHour: 10,
pricePerDay: 50,
};
@@ -340,7 +340,7 @@ describe('Rentals Routes', () => {
});
it('should return 400 for unavailable item', async () => {
Item.findByPk.mockResolvedValue({ ...mockItem, availability: false });
Item.findByPk.mockResolvedValue({ ...mockItem, isAvailable: false });
const response = await request(app)
.post('/rentals')
@@ -382,7 +382,7 @@ describe('Rentals Routes', () => {
Item.findByPk.mockResolvedValue({
id: 1,
ownerId: 2,
availability: true,
isAvailable: true,
pricePerHour: 0,
pricePerDay: 0
});

View File

@@ -186,11 +186,6 @@ describe('PayoutService', () => {
it('should successfully process a rental payout', async () => {
const result = await PayoutService.processRentalPayout(mockRental);
// Verify status update to processing
expect(mockRental.update).toHaveBeenNthCalledWith(1, {
payoutStatus: 'processing'
});
// Verify Stripe transfer creation
expect(mockCreateTransfer).toHaveBeenCalledWith({
amount: 9500,
@@ -206,7 +201,7 @@ describe('PayoutService', () => {
});
// Verify status update to completed
expect(mockRental.update).toHaveBeenNthCalledWith(2, {
expect(mockRental.update).toHaveBeenCalledWith({
payoutStatus: 'completed',
payoutProcessedAt: expect.any(Date),
stripeTransferId: 'tr_123456789'
@@ -260,13 +255,8 @@ describe('PayoutService', () => {
await expect(PayoutService.processRentalPayout(mockRental))
.rejects.toThrow('Stripe transfer failed');
// Verify processing status was set
expect(mockRental.update).toHaveBeenNthCalledWith(1, {
payoutStatus: 'processing'
});
// Verify failure status was set
expect(mockRental.update).toHaveBeenNthCalledWith(2, {
expect(mockRental.update).toHaveBeenCalledWith({
payoutStatus: 'failed'
});
@@ -296,9 +286,7 @@ describe('PayoutService', () => {
});
const dbError = new Error('Database completion update failed');
mockRental.update
.mockResolvedValueOnce(true) // processing update succeeds
.mockRejectedValueOnce(dbError); // completion update fails
mockRental.update.mockRejectedValueOnce(dbError);
await expect(PayoutService.processRentalPayout(mockRental))
.rejects.toThrow('Database completion update failed');
@@ -315,16 +303,14 @@ describe('PayoutService', () => {
const updateError = new Error('Update failed status failed');
mockCreateTransfer.mockRejectedValue(stripeError);
mockRental.update
.mockResolvedValueOnce(true) // processing update succeeds
.mockRejectedValueOnce(updateError); // failed status update fails
mockRental.update.mockRejectedValueOnce(updateError);
// The service will throw the update error since it happens in the catch block
await expect(PayoutService.processRentalPayout(mockRental))
.rejects.toThrow('Update failed status failed');
// Should still attempt to update to failed status
expect(mockRental.update).toHaveBeenNthCalledWith(2, {
expect(mockRental.update).toHaveBeenCalledWith({
payoutStatus: 'failed'
});
});

View File

@@ -1,32 +1,17 @@
import React from 'react';
interface RulesFormProps {
needsTraining: boolean;
rules: string;
onChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
}
const RulesForm: React.FC<RulesFormProps> = ({
needsTraining,
rules,
onChange
}) => {
return (
<div className="card mb-4">
<div className="card-body">
<div className="form-check mb-3">
<input
type="checkbox"
className="form-check-input"
id="needsTraining"
name="needsTraining"
checked={needsTraining}
onChange={onChange}
/>
<label className="form-check-label" htmlFor="needsTraining">
Requires in-person training before rental
</label>
</div>
<label htmlFor="rules" className="form-label">
Additional Rules
</label>

View File

@@ -30,7 +30,6 @@ interface ItemFormData {
latitude?: number;
longitude?: number;
rules?: string;
needsTraining: boolean;
generalAvailableAfter: string;
generalAvailableBefore: string;
specifyTimesPerDay: boolean;
@@ -63,7 +62,6 @@ const CreateItem: React.FC = () => {
state: "",
zipCode: "",
country: "US",
needsTraining: false,
generalAvailableAfter: "09:00",
generalAvailableBefore: "17:00",
specifyTimesPerDay: false,
@@ -483,7 +481,6 @@ const CreateItem: React.FC = () => {
/>
<RulesForm
needsTraining={formData.needsTraining}
rules={formData.rules || ""}
onChange={handleChange}
/>

View File

@@ -211,15 +211,15 @@ const EarningsDashboard: React.FC = () => {
className={`badge ${
rental.payoutStatus === "completed"
? "bg-success"
: rental.payoutStatus === "processing"
? "bg-warning"
: rental.payoutStatus === "failed"
? "bg-danger"
: "bg-secondary"
}`}
>
{rental.payoutStatus === "completed"
? "Paid"
: rental.payoutStatus === "processing"
? "Processing"
: rental.payoutStatus === "failed"
? "Failed"
: "Pending"}
</span>
</td>

View File

@@ -30,7 +30,6 @@ interface ItemFormData {
latitude?: number;
longitude?: number;
rules?: string;
needsTraining: boolean;
generalAvailableAfter: string;
generalAvailableBefore: string;
specifyTimesPerDay: boolean;
@@ -87,7 +86,6 @@ const EditItem: React.FC = () => {
zipCode: "",
country: "US",
rules: "",
needsTraining: false,
generalAvailableAfter: "09:00",
generalAvailableBefore: "17:00",
specifyTimesPerDay: false,
@@ -149,7 +147,6 @@ const EditItem: React.FC = () => {
latitude: item.latitude,
longitude: item.longitude,
rules: item.rules || "",
needsTraining: item.needsTraining || false,
generalAvailableAfter: item.availableAfter || "09:00",
generalAvailableBefore: item.availableBefore || "17:00",
specifyTimesPerDay: item.specifyTimesPerDay || false,
@@ -547,7 +544,6 @@ const EditItem: React.FC = () => {
</div>
<RulesForm
needsTraining={formData.needsTraining}
rules={formData.rules || ""}
onChange={handleChange}
/>

View File

@@ -633,7 +633,7 @@ const ItemDetail: React.FC = () => {
})()}
{/* Rental Period Selection - Only show for non-owners */}
{!isOwner && item.availability && !isAlreadyRenting && (
{!isOwner && item.isAvailable && !isAlreadyRenting && (
<>
<hr />
<div className="text-start">
@@ -758,7 +758,7 @@ const ItemDetail: React.FC = () => {
)}
{/* Action Buttons */}
{!isOwner && item.availability && !isAlreadyRenting && (
{!isOwner && item.isAvailable && !isAlreadyRenting && (
<div className="d-grid">
<button
className="btn btn-primary"

View File

@@ -48,7 +48,7 @@ const ItemList: React.FC = () => {
// Access the items array from response.data.items
const allItems = response.data.items || response.data || [];
// Filter only available items
const availableItems = allItems.filter((item: Item) => item.availability);
const availableItems = allItems.filter((item: Item) => item.isAvailable);
setItems(availableItems);
} catch (err: any) {
console.error("Error fetching items:", err);

View File

@@ -110,11 +110,11 @@ const Owning: React.FC = () => {
try {
await api.put(`/items/${item.id}`, {
...item,
availability: !item.availability,
isAvailable: !item.isAvailable,
});
setListings(
listings.map((i) =>
i.id === item.id ? { ...i, availability: !i.availability } : i
i.id === item.id ? { ...i, isAvailable: !i.isAvailable } : i
)
);
} catch (err: any) {
@@ -536,10 +536,10 @@ const Owning: React.FC = () => {
<div className="mb-2 d-flex gap-2 flex-wrap">
<span
className={`badge ${
item.availability ? "bg-success" : "bg-secondary"
item.isAvailable ? "bg-success" : "bg-secondary"
}`}
>
{item.availability ? "Available" : "Not Available"}
{item.isAvailable ? "Available" : "Not Available"}
</span>
{item.isDeleted && (
<span className="badge bg-danger">
@@ -620,7 +620,7 @@ const Owning: React.FC = () => {
onClick={() => toggleAvailability(item)}
className="btn btn-sm btn-outline-info"
>
{item.availability
{item.isAvailable
? "Mark Unavailable"
: "Mark Available"}
</button>

View File

@@ -111,7 +111,7 @@ const RentItem: React.FC = () => {
setItem(response.data);
// Check if item is available
if (!response.data.availability) {
if (!response.data.isAvailable) {
setError("This item is not available for rent");
}

View File

@@ -89,12 +89,8 @@ export interface Item {
longitude?: number;
images: string[];
condition: "excellent" | "good" | "fair" | "poor";
availability: boolean;
specifications: Record<string, any>;
isAvailable: boolean;
rules?: string;
minimumRentalDays: number;
maximumRentalDays?: number;
needsTraining?: boolean;
availableAfter?: string;
availableBefore?: string;
specifyTimesPerDay?: boolean;
@@ -151,7 +147,7 @@ export interface Rental {
stripePaymentIntentId?: string;
stripePaymentMethodId?: string;
// Payout status tracking
payoutStatus?: "pending" | "processing" | "completed" | "failed";
payoutStatus?: "pending" | "completed" | "failed" | null;
payoutProcessedAt?: string;
stripeTransferId?: string;
deliveryMethod: "pickup" | "delivery";