Background
The Customer Reviews team at a large online retailer lets shoppers mark reviews as helpful or not helpful, and wants the most useful reviews at the top of each product page. Posting and voting already work (mostly); the ranking that orders a product's reviews was left unfinished when its author moved teams.
How the system works
Reviews and votes
- A review belongs to a product and has an author, 1–5 stars, text, a posting date and a
verifiedflag (the author bought the product here). Ids areR-1,R-2, ... in posting order. Stars outside 1–5 are400 INVALID_STARS. - A shopper has at most one vote per review: voting again on the same review replaces their earlier vote (so they can change their mind). Votes on different reviews are independent.
- Authors can't vote on their own review (
403 OWN_REVIEW). Voting on an unknown review is404 NOT_FOUND. - Removing a vote that doesn't exist is
404 NO_VOTE. - A review's tally is its number of helpful and not-helpful votes.
Ranking
RankingService.rank(productId) returns the product's reviews for display:
- Score =
2 × helpful − notHelpful, plus 3 for a verified review. (RankingService.scorecomputes this.) - Buried reviews are left out: those with at least 5 not-helpful votes and more than twice as many not-helpful as helpful votes (
RankingService.isBuried). - The rest are sorted:
- higher score first;
- then more total votes (helpful + not helpful) first;
- then the newer posting date first;
- then the smaller id (plain string order, so
R-10comes beforeR-9).
- Each entry carries the review, its helpful and not-helpful counts, and its score. A product without reviews gives an empty list.
Example: R-1 with 3 helpful votes (score 6), verified R-2 with 1 helpful vote (score 5) and R-3 with none (score 0) rank R-1, R-2, R-3.
API
| Method and path | Body | Success |
|---|---|---|
POST /api/products/{productId}/reviews | {stars, text, verified, postedOn} | 201 review |
GET /api/products/{productId}/reviews | 200 ranked reviews: [{review, helpful, notHelpful, score}] | |
POST /api/reviews/{id}/votes | {helpful: true/false} | 200 tally {helpful, notHelpful} |
DELETE /api/reviews/{id}/votes | 204 |
Posting and voting need an X-Customer-Id header (the author or the voter). A missing header, missing helpful, blank text, missing postedOn or malformed JSON is 400 INVALID_REQUEST. Errors return {"error": "<CODE>", "message": "..."}.
Project structure
src/main/java/com/bugnik/reviewvotes/
ReviewVotesApplication.java Spring Boot entry point
model/ Review, Vote, VoteTally, RankedReview
repository/ in-memory ReviewRepository and VoteRepository
service/ReviewService.java posting, voting, removing votes, tallies
service/ReviewRanking.java the ranking interface the controller uses
service/RankingService.java score, isBuried and rank (the ranking)
service/ReviewException.java business errors (status + code)
web/ ReviewController, ApiError, ApiExceptionHandler
src/test/java/com/bugnik/reviewvotes/ unit and MockMvc tests
Your task
Implement RankingService.rank as specified in "Ranking", and fix any defects that stop the test suite from passing.
- Do not modify
src/test/orpom.xml. - Keep your changes minimal. Fix the defects; don't rewrite modules.
- Additional hidden tests check the behaviour described in this README.