Background
The Payments team at a large online retailer runs the gift card wallet: customers load cards, check balances and spend them at checkout. Since the last release, customers can't see or spend their own cards through the API, and cards are refused on their expiry date even though the terms say they're valid through that day.
How the system works
Card codes
A gift card code has 12 letters or digits. Customers type it any way they like (abcd-efgh 2345), so every code is normalized first: spaces and dashes removed, then upper-cased (ABCDEFGH2345). Any other character, or a length other than 12 after normalizing, is invalid. Cards are stored and returned under their normalized code.
Rules
- Issue: a new card needs a valid code, an owner (customer id) and an amount from 1 cent to $2,000.00 (200000 cents). It starts
ACTIVE. A code that already exists is rejected. - Ownership: every call except issuing names the customer in the
X-Customer-Idheader. Only the card's owner may read or use it; customer ids are compared exactly (case-sensitive). - Expiry: a card is valid through its expiry date: it can be used on that date and stops working the day after. "Today" comes from the service's clock (UTC).
- Redeem: takes a positive amount from an
ACTIVE, unexpired card. The amount may equal the balance (leaving 0) but not exceed it. Each redemption gets a transaction id (T-1,T-2, ...). - Idempotency: a redemption is identified by the card and the
orderId. Redeeming the same order on the same card again returns the original redemption and changes nothing (even if the amount differs). The same order id on a different card is a separate redemption. - Reload: adds a positive amount to an
ACTIVE, unexpired card; the balance may reach but not exceed $2,000.00. - Freeze: sets the card to
FROZEN. A frozen card can't be redeemed or reloaded. Freezing an already frozen card is fine. - Checks happen in this order: card exists → owner → (for redemptions) repeated order → amount → frozen → expired → balance or limit.
API
| Method and path | Body | Success |
|---|---|---|
POST /api/gift-cards | {code, ownerId, amountCents, expiresOn} | 201 card |
GET /api/gift-cards/{code} | 200 card | |
POST /api/gift-cards/{code}/redemptions | {orderId, amountCents} | 201 redemption; 200 with the original for a repeated order |
POST /api/gift-cards/{code}/reloads | {amountCents} | 200 card |
POST /api/gift-cards/{code}/freeze | 200 card |
- A card is
{code, ownerId, balanceCents, expiresOn, status}(expiresOnasYYYY-MM-DD). - A redemption is
{transactionId, code, orderId, amountCents, balanceAfterCents}. {code}in the path may be typed in any format; it's normalized.
Errors
Errors return {"error": "<CODE>", "message": "..."}:
| Status | Codes |
|---|---|
400 | INVALID_CODE, INVALID_AMOUNT, INVALID_REQUEST (missing header, missing/blank fields, malformed JSON) |
403 | FORBIDDEN (not the owner) |
404 | NOT_FOUND (unknown card) |
409 | DUPLICATE_CODE, INSUFFICIENT_FUNDS, BALANCE_LIMIT, CARD_FROZEN, CARD_EXPIRED |
Project structure
src/main/java/com/bugnik/giftcards/
GiftCardsApplication.java Spring Boot entry point
config/TimeConfig.java the Clock bean ("today")
model/ GiftCard, CardStatus, Redemption
repository/ in-memory GiftCardRepository and RedemptionRepository
service/GiftCardService.java the rules above
service/GiftCardException.java business errors (status + code)
util/CardCodes.java code normalization and validation
web/ GiftCardController, request bodies, ApiError, ApiExceptionHandler
src/test/java/com/bugnik/giftcards/ unit and MockMvc tests
Your task
Customers and the support team have reported problems since the last release:
- The project no longer builds.
- Customers get
403 FORBIDDENwhen they look up or spend their own gift card through the API. - A card whose expiry date is today is refused as expired.
Find and fix the defects so that all tests pass.
- 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.