Skip to content

Commit 8fd0ad7

Browse files
authored
feat: implement backend for bids (#45)
2 parents bad82f8 + b67f838 commit 8fd0ad7

7 files changed

Lines changed: 182 additions & 3 deletions

File tree

backend/src/db/models/bid.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import mongoose, { Schema } from 'mongoose';
2+
import type { InferSchemaType } from 'mongoose';
3+
4+
// Create the DB schema for auction bids.
5+
const bidSchema = new Schema(
6+
{
7+
userId: { type: Schema.Types.ObjectId, ref: 'user', required: true },
8+
amount: { type: Number, required: true },
9+
auctionId: { type: Schema.Types.ObjectId, ref: 'auction', required: true },
10+
},
11+
{
12+
timestamps: true,
13+
},
14+
);
15+
16+
export const Bid = mongoose.model('bid', bidSchema);
17+
export type BidDataType = InferSchemaType<typeof bidSchema>;

backend/src/routes/auctions.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { requireAuth } from '../middleware/jwt.ts';
44
import AuctionsService from '../services/auctions.ts';
55
import {
66
createAuctionSchema,
7+
createBidSchema,
78
updateAuctionSchema,
89
} from 'treasure-trove-shared';
10+
import BidsService from '../services/bids.ts';
911

1012
const auctionsRouter = express.Router();
1113

@@ -62,4 +64,38 @@ auctionsRouter.put('/:id', requireAuth, async (req: Request, res: Response) => {
6264
}
6365
});
6466

67+
// GET all the bids associated with a given auction.
68+
auctionsRouter.get(
69+
'/:id/bids',
70+
requireAuth,
71+
async (req: Request, res: Response) => {
72+
try {
73+
const bidsInfo = await BidsService.getAuctionBids(req.params.id);
74+
return res.status(200).json(bidsInfo);
75+
} catch (error) {
76+
console.error('Error fetching auction bids:', error);
77+
return res.status(404).json({ error: "Auction's bids not found" });
78+
}
79+
},
80+
);
81+
82+
// POST endpoint to create a new bid for the given auction.
83+
auctionsRouter.post(
84+
'/:id/bids',
85+
requireAuth,
86+
async (req: Request, res: Response) => {
87+
try {
88+
const validatedBody = createBidSchema.validateSync(req.body);
89+
const bid = await BidsService.createBid(validatedBody);
90+
// Use 201 when a POST request successfully creates a new resource on the server.
91+
return res.status(201).json(bid);
92+
} catch (error) {
93+
console.error('Auction creation error:', error);
94+
return res.status(400).json({
95+
error: 'Failed to create auction',
96+
});
97+
}
98+
},
99+
);
100+
65101
export default auctionsRouter;

backend/src/services/auctions.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ class AuctionsService {
99
// Fetch information about all auctions in the database.
1010
static async getAllAuctions(): Promise<AuctionInfo[]> {
1111
const auctions = await Auction.find({});
12-
return auctions.map((a) =>
13-
AuctionsService.parseAuctionInfo(a._id.toString(), a),
14-
);
12+
return auctions.map((a) => this.parseAuctionInfo(a._id.toString(), a));
1513
}
1614

1715
// Fetch information about a particular auction.

backend/src/services/bids.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { BidInfo, CreateBidInfo } from '@shared/bids.ts';
2+
import { Bid, type BidDataType } from '../db/models/bid.ts';
3+
import AuctionsService from './auctions.ts';
4+
5+
class BidsService {
6+
// Get all the bids that have been made on a given auction.
7+
static async getAuctionBids(auctionId: string): Promise<BidInfo[]> {
8+
const bids = await Bid.find({ auctionId });
9+
return bids.map((b) => this.parseBidInfo(b._id.toString(), b));
10+
}
11+
12+
// Make a new bid on an auction.
13+
static async createBid(bidInfo: CreateBidInfo): Promise<BidInfo> {
14+
const auction = await AuctionsService.getAuctionById(bidInfo.auctionId);
15+
16+
// Make sure the bid is at least as high as the minimum bid.
17+
// We can make this preliminary check before going through all of the other bids
18+
// for a slight performance boost.
19+
if (bidInfo.amount < auction.minimumBid) {
20+
throw new Error('bid amount must be at least the minimum bid');
21+
}
22+
// If the bid as at least as high as the minimum bid, then make sure it is higher
23+
// than all other bids that have been made.
24+
const currHighBid = await this.getCurrentHighestBid(bidInfo.auctionId);
25+
if (currHighBid && currHighBid.amount >= bidInfo.amount) {
26+
throw new Error('bid amount must be higher than the previous bid');
27+
}
28+
29+
// If the bid amount is valid, then create the new bid.
30+
const bid = new Bid({
31+
userId: bidInfo.userId,
32+
amount: bidInfo.amount,
33+
auctionId: bidInfo.auctionId,
34+
});
35+
await bid.save();
36+
return this.parseBidInfo(bid._id.toString(), bid);
37+
}
38+
39+
// Figure out what the current highest bid is on the given auction.
40+
private static async getCurrentHighestBid(
41+
auctionId: string,
42+
): Promise<BidInfo | undefined> {
43+
const bids = await this.getAuctionBids(auctionId);
44+
if (bids.length === 0) return undefined;
45+
46+
let maxBid = bids[0];
47+
for (let i = 1; i < bids.length; i++) {
48+
if (bids[i].amount > maxBid.amount) {
49+
maxBid = bids[i];
50+
}
51+
}
52+
53+
return maxBid;
54+
}
55+
56+
// This function is used for taking an auction DB document and converting it to
57+
// a usable interface.
58+
private static parseBidInfo(bidId: string, bid: BidDataType): BidInfo {
59+
return {
60+
id: bidId,
61+
userId: bid.userId.toString(),
62+
amount: bid.amount,
63+
auctionId: bid.auctionId.toString(),
64+
createdDate: bid.createdAt,
65+
};
66+
}
67+
}
68+
69+
export default BidsService;

frontend/src/api/auctions.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference types="vite/client" />
22

3+
import { BidInfo, bidInfoSchema, CreateBidInfo } from '@shared/bids.ts';
34
import { apiRoute, jwtHeaders } from './utils';
45
import {
56
AuctionInfo,
@@ -84,6 +85,41 @@ class AuctionsApi {
8485
const body = auctionInfoSchema.validateSync(rawBody);
8586
return body;
8687
}
88+
89+
// Get the bidding history for the given auction.
90+
static async getAuctionBids(id: string, token: string): Promise<BidInfo[]> {
91+
const res = await fetch(apiRoute(`auctions/${id}/bids`), {
92+
method: 'GET',
93+
headers: {
94+
'Content-Type': 'application/json',
95+
...jwtHeaders(token),
96+
},
97+
});
98+
if (!res.ok) throw new Error('failed to fetch auction bids');
99+
const rawBody: BidInfo[] = await res.json();
100+
const body = rawBody.map((a) => bidInfoSchema.validateSync(a));
101+
return body;
102+
}
103+
104+
// Make a bid on an auction.
105+
static async makeBid(
106+
auctionId: string,
107+
newBid: CreateBidInfo,
108+
token: string,
109+
): Promise<BidInfo[]> {
110+
const res = await fetch(apiRoute(`auctions/${auctionId}/bids`), {
111+
method: 'POST',
112+
headers: {
113+
'Content-Type': 'application/json',
114+
...jwtHeaders(token),
115+
},
116+
body: JSON.stringify(newBid),
117+
});
118+
if (!res.ok) throw new Error('failed to make bid');
119+
const rawBody: BidInfo[] = await res.json();
120+
const body = rawBody.map((a) => bidInfoSchema.validateSync(a));
121+
return body;
122+
}
87123
}
88124

89125
export default AuctionsApi;

shared/src/bids.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as yup from 'yup';
2+
3+
// Creating a bid requires inputting these fields.
4+
const createBidSchema = yup.object({
5+
userId: yup.string().required(),
6+
amount: yup.number().required().min(0),
7+
auctionId: yup.string().required(),
8+
});
9+
10+
// When the backend returns information about a bid, it contains
11+
// all of the above fields plus bid ID and the creation date.
12+
const bidInfoSchema = createBidSchema.concat(
13+
yup.object({
14+
id: yup.string().required(),
15+
createdDate: yup.date().required(),
16+
}),
17+
);
18+
19+
export type CreateBidInfo = yup.InferType<typeof createBidSchema>;
20+
export type BidInfo = yup.InferType<typeof bidInfoSchema>;
21+
22+
export { createBidSchema, bidInfoSchema };

shared/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
export * from './auth.ts';
33
export * from './users.ts';
44
export * from './auctions.ts';
5+
export * from './bids.ts';

0 commit comments

Comments
 (0)