An AI assistant opened a pull request for the shop: customers can send a discount code to POST /products/{productId}/discount and get the price after the discount, in cents. It works for SPRING10 on the Lamp, so it looks done. Here is the AI's method:
@PostMapping("/products/{productId}/discount")
Object applyDiscount(@PathVariable int productId, @RequestBody Map<String, Object> data) {
List<Map<String, Object>> products = db.queryForList("SELECT * FROM products WHERE id = ?", productId);
Map<String, Object> product = products.get(0);
Map<String, Object> discount = db.queryForList("SELECT * FROM discounts WHERE code = '" + data.get("code") + "'").get(0);
int totalCents = (int) product.get("price") * (100 - (int) discount.get("percent"));
return Map.of("product", product.get("name"), "code", discount.get("code"), "total_cents", totalCents);
}
A review asks what the AI never tried. Missing: product 99 or the code SUMMER50 gives an empty list, get(0) throws, and Spring answers 500. Hostile: the + glues the customer's text into the SQL, so the code x' OR '1'='1 matches every row and gets SPRING10's discount. That is SQL injection. Twice: every code has uses_left, and the AI never reads it, so the one-use VIP25 works forever and the used-up WINTER15 still works.
You know each fix from earlier lessons. Here they are on another table: a ? placeholder keeps typed text a plain value, and one UPDATE with a condition takes a unit only if one is left. Then the number db.update returns tells you whether it did.
List<Map<String, Object>> users = db.queryForList("SELECT * FROM users WHERE email = ?", email);
int updated = db.update("UPDATE products SET stock = stock - 1 WHERE id = ? AND stock > 0", productId);
if (updated == 0) {
return ResponseEntity.status(409).body(Map.of("error", "Out of stock"));
}
The codes: SPRING10 (10% off, 100 uses), VIP25 (25% off, 1 use) and WINTER15 (15% off, used up). Sent in this order, the requests must get these answers:
POST /products/3/discount {"code": "SPRING10"} → 200 {"code": "SPRING10", "product": "Lamp", "total_cents": 2610}
POST /products/99/discount {"code": "SPRING10"} → 404 {"error": "Product not found"}
POST /products/3/discount {"code": "SUMMER50"} → 404 {"error": "Unknown code"}
POST /products/3/discount {"code": "x' OR '1'='1"} → 404 {"error": "Unknown code"}
POST /products/1/discount {"code": "VIP25"} → 200 {"code": "VIP25", "product": "Mug", "total_cents": 900}
POST /products/1/discount {"code": "VIP25"} → 409 {"error": "Code used up"}
POST /products/5/discount {"code": "WINTER15"} → 409 {"error": "Code used up"}
Your task: the database, the discounts table, the route, the method's first line and the product lookup are ready. Write the rest of applyDiscount so it fixes all three problems: 404 for a missing product or code, a ? placeholder for the code, and one atomic UPDATE that takes a use, with 409 when none is left. Keep the AI's total in cents.
Assignment
Follow the Coding Tutorial to rewrite applyDiscount one problem at a time: the two 404s, the placeholder, the atomic UPDATE with its 409, and the answer.
Hint
Check in this order: the product, then the code, then the use, then the price. Take a use only after both lookups found a row, so a request for a missing product never spends a code. Until the last step the method has no return at its end, so the code won't compile before then.
Free plan: 30 tutor messages. PRO is unlimited.
