Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Customer Rewards API

A Spring Boot REST API that calculates monthly and overall reward points for customers based on their transaction history.

Business Rule

For each transaction:

  • 0 points on the portion of the amount at or below $50
  • 1 point per dollar on the portion between $50 and $100
  • 2 points per dollar on the portion above $100

Example: a $120 purchase earns (100-50)×1 + (120-100)×2 = 90 points.

Assumptions

The prompt leaves a few things unstated. Rather than guess silently, here's what this implementation assumes and why:

  1. Spend at or below $50 earns 0 points. Not stated explicitly, but required by the worked example: a $120 purchase yields exactly 90 points, which only holds if the first $50 contributes nothing.
  2. Tier boundaries are resolved by formula, not by picking inclusive/exclusive bounds. Points are computed as max(min(amount,100)-50, 0)×1 + max(amount-100, 0)×2. This makes $50.00 and $100.00 exactly well-defined without an arbitrary inclusive/exclusive choice at the boundary.
  3. Points are rounded per transaction, not per month or per customer. Each transaction's raw point value is rounded to the nearest whole number (HALF_UP) before being summed, matching how a real system would post points at the time of purchase.
  4. Non-positive amounts (refunds, $0 transactions) earn 0 points rather than subtracting points. The prompt doesn't mention refunds, so this implementation treats them as reward-neutral rather than inferring a clawback policy.
  5. A customer ID with no transactions in the dataset returns HTTP 404. There's no separate customer registry in this assessment — only transaction records — so an unknown customer ID and a known customer with zero transactions in the period are indistinguishable.
  6. Months are grouped by the transaction's calendar date, not a billing cycle, since no billing cycle is defined.
  7. Currency is treated as a single, unspecified currency — no multi-currency or FX handling.
  8. A transaction amount whose calculated points would be too large to fit in a standard integer (well beyond any realistic retail purchase) fails loudly with ArithmeticException rather than silently producing a wrong number — kept as a one-line guard rather than a dedicated error type, since that scenario can't realistically occur with retail-scale amounts.
  9. An unmapped route (e.g. /rewards/ with a trailing slash) correctly returns 404, not 500. Spring throws NoResourceFoundException for any request that matches no endpoint, and that's an Exception like any other — a purely generic catch-all handler would have swallowed it and misreported it as a server error, so it's handled explicitly ahead of the catch-all.

Architecture

controller  →  service  →  repository
                  ↓
              domain (RewardCalculator)
  • domain.RewardCalculator — the entire points formula, as a pure static method with no Spring dependency. This is deliberate: it's the one piece of logic where correctness really matters, so it's isolated and unit-testable with nothing else running.
  • service.RewardServiceImpl — groups transactions by customer and calendar month and sums points. Contains no point-calculation logic of its own; it only orchestrates.
  • repository.TransactionRepository — an interface with an in-memory implementation for this assessment. Transactions are indexed by customerId in a map at load time, so a lookup is O(1) average case instead of scanning every transaction in the dataset on every request. A real deployment would swap this implementation for a JPA/database-backed one behind the same interface with no changes to the service or controller layers.
  • config.SampleDataLoader — loads sample-transactions.json into the repository at startup via CommandLineRunner, so the API has data immediately with no manual setup step.
  • model.Transaction / dto.* — Java 17 records, used for immutability and to avoid getter/setter/equals/hashCode boilerplate.
  • exception.GlobalExceptionHandler — centralizes error handling via @RestControllerAdvice so every failure returns a consistent JSON shape instead of a raw stack trace or an inconsistent ad hoc error body.
  • BigDecimal is used for every currency calculation to avoid the rounding errors that come with double/float for money.

API

Base path: /api/v1/rewards

Method Path Description
GET /customers Monthly breakdown + total for every customer
GET /customers/{customerId} Monthly breakdown + total for one customer
GET /customers/{customerId}/monthly Just the monthly breakdown for one customer

Example

GET /api/v1/rewards/customers/CUST001
{
  "customerId": "CUST001",
  "monthlyBreakdown": [
    { "month": "2026-01", "points": 115 },
    { "month": "2026-02", "points": 50 },
    { "month": "2026-03", "points": 251 }
  ],
  "totalPoints": 416
}

Unknown customer:

GET /api/v1/rewards/customers/DOES-NOT-EXIST
→ 404 Not Found
{
  "timestamp": "...",
  "status": 404,
  "message": "No transaction history found for customer: DOES-NOT-EXIST"
}

Unmapped route (e.g. a stray trailing slash, or any URL nothing is mapped to):

GET /api/v1/rewards/
→ 404 Not Found
{
  "timestamp": "...",
  "status": 404,
  "message": "No endpoint found for the requested path."
}

Sample Dataset

src/main/resources/sample-transactions.json contains 14 transactions across 3 customers (CUST001, CUST002, CUST003) and 3 months (Jan–Mar 2026), including boundary values ($50.00, $100.00 exactly) and decimal-cent amounts to exercise the rounding logic.

Expected totals, for reference when reviewing test output:

Customer Jan Feb Mar Total
CUST001 115 50 251 416
CUST002 150 31 500 681
CUST003 0 10 121 131

Running Locally

Requires Java 17+ and Maven (or use the included mvnw wrapper if you add one — this repo assumes a local Maven install).

# Run the app (starts on http://localhost:8080)
mvn spring-boot:run

# Run the full test suite
mvn test

# Build a runnable jar
mvn clean package
java -jar target/rewards-api-1.0.0.jar

Tech Stack

  • Java 17
  • Spring Boot 3.3.4 (Web, Test)
  • JUnit 5, Mockito, AssertJ

Possible Future Enhancements

Out of scope for this assessment, but what I'd raise in a design review for a production version:

  • Swap InMemoryTransactionRepository for a JPA-backed repository and real database
  • POST endpoint to ingest new transactions instead of a static bundled file
  • Pagination and date-range filtering on the customer list/monthly endpoints
  • OpenAPI/Swagger documentation
  • AuthN/AuthZ on the endpoints

About

Spring Boot REST API that calculates customer reward points from transaction history, with monthly and overall totals.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages