Bugnik
← All problems

04Marketplace Seller Payouts API

Java · Spring BootMediumBug fixSuggested time: 60 min

Seller payout ledger REST API (CSV import, fees, auth)

Sign in to start

Before you start

You'll get more out of this problem if you're comfortable with:

Language

  • Java primitive types and reading javac / Maven compile errors
  • Java Strings, records, enums and switch expressions

Libraries and frameworks

  • Spring Boot REST basics: controllers, services, @Component, constructor injection, @Value
  • Path variables, query parameters and request headers in Spring MVC; HTTP 400 / 403 / 404

Build and test tools

  • Reading JUnit 5 / MockMvc test failures

Reasoning

  • Money as integer cents, parsing decimal strings, rounding half up
  • LocalDate and half-open date ranges [from, to)

Background

The Marketplace Payments team at a large online retailer pays third-party sellers every month. A nightly job uploads the ledger (sales, refunds and manual adjustments) to this service, and sellers open their payout statement in the seller portal. After the latest release, sellers say they can't see their own statements, the finance team's month-end numbers don't match, and the build is broken.

How the system works

Ledger file

The ledger is uploaded as CSV text, one id,sellerId,date,type,amount row per line:

id,sellerId,date,type,amount
E2,S-100,2026-09-01,SALE,125.60
E4,S-100,2026-09-20,REFUND,20.00
E5,S-100,2026-09-28,ADJUSTMENT,-15.25
  • If the first non-blank line starts with id (any case), it is a header and is skipped. Blank lines are ignored and every field is trimmed.
  • Each row has exactly 5 fields. id and sellerId must not be empty. Seller ids are case-sensitive (S-100 and s-100 are different sellers).
  • date is YYYY-MM-DD and must be a real date.
  • type is SALE, REFUND or ADJUSTMENT (any case).
  • amount is a money string: an optional leading -, whole units, and optionally . plus one or two digits (125.60, 12.5, 7, -15.25). No +, no thousands separators, no more than two decimals. A negative amount is negative in both its units and its cents: -3.25 is −325 cents.
  • SALE and REFUND amounts must be greater than zero (a refund is written as a positive amount). ADJUSTMENT amounts may be positive or negative but not zero.
  • An id may appear only once per file.
  • Any invalid row throws InvalidRequestException and rejects the whole file.

Importing

  • Imports are all-or-nothing: if any row is invalid, or conflicts with a stored entry, nothing from that file is stored.
  • An entry whose id is already stored with identical content is skipped. This makes retried uploads safe. The same id with different content rejects the whole file.
  • The result reports added (new entries stored) and skipped (identical repeats).

Marketplace fee

The fee is configured in basis points (payouts.fee-bps, default 800 = 8.00%). It is charged on each sale separately: fee = saleCents × feeBps / 10000, rounded half up to a whole cent. Refunds and adjustments carry no fee and get no fee back.

Payout statement

summarize(sellerId, from, to) covers entries dated from from (inclusive) to to (exclusive):

  • from must be strictly before to, else InvalidRequestException.
  • If the seller has no entries at all, NotFoundException. A seller with entries, just none in the period, gets a statement of zeros.
  • entryCount: entries in the period.
  • salesCents: sum of SALE amounts. refundsCents: sum of REFUND amounts (a positive number). feesCents: sum of the per-sale fees. adjustmentsCents: sum of ADJUSTMENT amounts (signed).
  • netCents = salesCents − refundsCents − feesCents + adjustmentsCents. It can be negative.

Example (fee 800 bps): sales 125.60 and 40.00, refund 20.00, adjustment −15.25 → sales 16560, fees 1005 + 320 = 1325, refunds 2000, adjustments −1525, net 11710.

API

Method + pathSuccessNotes
POST /api/ledger/import (body: CSV, Content-Type: text/plain)200 + {"added","skipped"}
GET /api/sellers/{sellerId}/payout?from=YYYY-MM-DD&to=YYYY-MM-DD200 + {"sellerId","from","to","entryCount","salesCents","refundsCents","feesCents","adjustmentsCents","netCents"}requires header X-Seller-Id

Authorization: a seller may only read their own statement. The X-Seller-Id header must be present and exactly equal to {sellerId}; otherwise the response is 403.

Errors return {"error": "<CODE>", "message": "<detail>"}:

SituationHTTP statuserror
NotFoundException404NOT_FOUND
missing or different X-Seller-Id403FORBIDDEN
InvalidRequestException, or a missing/malformed from/to400INVALID_REQUEST

Project structure

src/main/java/com/bugnik/payouts/
  PayoutsApplication.java     Spring Boot entry point
  model/        EntryType, LedgerEntry, PayoutSummary, ImportResult
  repository/   LedgerRepository (in-memory)
  service/      LedgerService (imports), PayoutService (statements), FeeSchedule, exceptions
  util/         AmountParser (money strings), LedgerCsvParser (ledger file)
  web/          PayoutController, ApiError, ApiExceptionHandler
src/main/resources/application.properties   fee configuration
src/test/java/...             unit tests and MockMvc API tests
pom.xml                       Maven build (Spring Boot 3.3, Java 17)

Your task

Sellers and the finance team have reported problems since the last release:

  • The project no longer builds.
  • Sellers get "forbidden" when they open their own payout statement.
  • Month-end statements pay sellers more than they should whenever a buyer was refunded.
  • Some manual corrections from the payments team are booked for the wrong amount.

Find and fix the defects so that all tests pass.

  • Do not modify anything in src/test/ or pom.xml.
  • Keep your changes minimal. Fix the defects; don't rewrite modules.
  • Additional hidden tests check the behaviour described in this README.