diff --git a/README.md b/README.md index cb96007..ca461cd 100644 --- a/README.md +++ b/README.md @@ -3,54 +3,93 @@ ## Introduction Thanks for applying for a development role at Fatsoma. To give us a good -indication of programming ability and style please submit your solution for -this ticket allocation problem. +indication of your programming ability and style, please submit your solution +for this ticket allocation problem. We are mainly looking for clean, well +architected, tested code that highlights your skill set and shows technical +proficiency. This is not a timed test, but you do not need to spend more than a couple of -hours on this. A partial solution is still very useful, and you can describe -your thoughts for next steps that would be taken. +hours on it. We have tried to keep the scope small so that time is spent on the +interesting parts rather than on boilerplate. A partial solution is still very +useful — if you run out of time, describe in your submission what you would do +next. Your submission must be your own work. +### Use of AI tools + +Using AI tools (e.g. GitHub Copilot, ChatGPT, Claude) is allowed — they are part +of how we work day to day. However, please **declare any AI co-authorship** in +your submission notes: briefly note where and how you used AI (for example, +scaffolding, generating tests, or rubber-ducking the locking approach). You +remain responsible for everything you submit, so make sure you understand and +can explain your solution — we will likely discuss it when we meet. + + ### Languages and frameworks -For reference, here at Fatsoma we primarily develop using ruby on rails and -golang. However feel free to solve this in whatever language you feel comfortable with and use a framework if desired. +For reference, here at Fatsoma we primarily develop using Ruby on Rails and +Go. However, feel free to solve this in whatever language you are most +comfortable with, and use a framework if you wish. We are interested in how you +structure and reason about the solution, not in any particular stack. -We are mainly looking for clean, well architected, tested code that highlights -your skill set and shows technical proficiency. +## Problem definition -### Database +The following three routes need to be built to enable allocating ticket options +to multiple purchases. They all correspond to the [JSON:API spec](https://jsonapi.org/). +If helpful, a list of JSON:API implementation libraries across various languages +is available at https://jsonapi.org/implementations/. We're not assessing strict +conformance to the spec, so minor differences in the output produced by your +chosen library are fine. -Included is an SQL dump from PostgreSQL. It is not required to use this but -should be helpful. You may need to amend this to add constraints. +A `ticket_option` is created with a fixed `allocation` — the total number of +tickets available to purchase. Each `purchase` draws a `quantity` of tickets +against a `ticket_option`. -PostgreSQL has all the functionality required for satisfying this problem set, -with some features introduced version 9.5 that may be of interest. You may -choose to use a different database engine that satisfies the requirements of -this problem. +The solution must guarantee the following invariant at all times: ---- +> The sum of purchased quantities for a `ticket_option` must never exceed its +> `allocation`, and the remaining availability must never drop below 0. -## Problem definition +The `allocation` value on a `ticket_option` represents its **total** capacity +and does not change once created; remaining availability is derived from the +allocation minus the quantities already purchased. The `GET` route returns the +`ticket_option` as created (i.e. the original `allocation`). -The following three routes need to be built to enable allocating of ticket -options to multiple purchases. They all correspond to the jsonapi spec (https://jsonapi.org/) +Expect requests to be made against this API concurrently, and design it to scale +horizontally: multiple instances of the program may run at once to handle large +numbers of requests. The invariant above must hold regardless of how many +requests, or how many running instances, are involved — including when many +purchases hit the same `ticket_option` at the same time. -The solution needs to ensure that the allocation does not drop below 0, -and the purchased amounts are not greater than the allocation given. +We use the term "purchase", but taking payment is out of scope. You do not need +to build authentication, payment handling, a UI, or anything beyond the three +routes described below. -Expect multiple requests to be made against this API concurrently. +### Database + +Included is an SQL dump from PostgreSQL (`database_structure.sql`). You are not +required to use it, but it should be a helpful starting point. Note that it +intentionally ships **without** integrity constraints (no foreign keys, nullable +columns, no `CHECK`s) — part of the exercise is deciding what constraints the +schema should have. -We use the term purchase but taking payment is out of scope for this problem. +The SQL dump is provided purely as an example. Whatever storage you use, it must +let you prevent overselling a `ticket_option`'s allocation under concurrent load +across multiple instances. Beyond that, you may use any database engine or +storage approach you like. -## Routes with Example Requests +## Routes with example requests -The Swagger definition and postman collection are also available in this repository for reference. +All requests and responses use the JSON:API media type +`application/vnd.api+json`. The Swagger definition +(`Ticket_Allocation.swagger.yaml`) and Postman collection +(`Ticket_Allocation.postman_collection.json`) are also available in this +repository for reference. -### Create Ticket Option +### Create ticket option -Create a ticket_option with an allocation of tickets available to purchase: +Create a `ticket_option` with an allocation of tickets available to purchase: `POST /ticket_options` @@ -69,7 +108,7 @@ Request Body: } ``` -Response Body: +Response Body (`201 Created`): ```json { @@ -85,7 +124,10 @@ Response Body: } ``` -### Get Ticket Option +`allocation` must be a non-negative integer. Requests with a missing `name` or a +negative/non-integer `allocation` should return a `4xx` error. + +### Get ticket option Get ticket option by id: @@ -93,7 +135,7 @@ Get ticket option by id: (No request body) -Response Body: +Response Body (`200 OK`): ```json { @@ -109,9 +151,13 @@ Response Body: } ``` -### Purchase from Ticket Option +A request for an id that does not exist should return a `404 Not Found`. -Purchase a quantity of tickets from the allocation of the given ticket_option and associate with a user (N.B. managing a user resource is not being looked at here, so an example id should be sufficient): +### Purchase from ticket option + +Purchase a quantity of tickets from the allocation of the given `ticket_option` +and associate it with a user (N.B. managing a user resource is not being looked +at here, so an example id is sufficient): `POST /purchases` @@ -140,12 +186,11 @@ Request body: } } } - ``` Response Body: -A 2xx status code must be returned on success. +A `201 Created` status code must be returned on success. ```json { @@ -173,7 +218,9 @@ A 2xx status code must be returned on success. } ``` -A 4xx status code must be returned on any request that attempts to purchase more tickets than are available. In this case, no tickets should be purchased for that request. Example error response given below +A `4xx` status code must be returned on any request that attempts to purchase +more tickets than are available. In this case, no tickets should be purchased +for that request. Example error response given below: ```json { @@ -190,3 +237,38 @@ A 4xx status code must be returned on any request that attempts to purchase more ] } ``` + +### Validation and error handling + +Please handle at least the following cases with an appropriate `4xx` status and +a JSON:API `errors` payload (the shape above is a good template): + +- `quantity` that is zero or negative. +- A purchase referencing a `ticket_option` id that does not exist + (`404 Not Found`). +- A request that would exceed the remaining allocation (`code: + invalid_purchase_quantity`, as above) — no tickets persisted. +- Missing required attributes on create (e.g. `name`, `allocation`). + +You do not need to exhaustively validate every field; we are more interested in +seeing a consistent, sensible approach than full coverage. + +## Submitting your solution + +Please include a short README in your submission covering: + +1. **How to run it** — setup steps, dependencies, and how to start the API + (including any database setup/migrations). +2. **How to run the tests.** +3. **Notes** — any assumptions you made, trade-offs, what you would do next + given more time, and a short declaration of any AI tool usage (see + [Use of AI tools](#use-of-ai-tools)). + +Submit your solution as a link to a Git repository (e.g. GitHub) or as a +zip/tarball, whichever is easier for you. If you use a private repository, please +invite the GitHub user `fatsoma-review` so we can access it. Please do not +include compiled artifacts or dependency directories (e.g. `node_modules`, +`vendor`). + +If anything in this brief is unclear, make a reasonable assumption, state it in +your notes, and carry on — we are happy to discuss your reasoning when we meet. diff --git a/Ticket_Allocation.swagger.yaml b/Ticket_Allocation.swagger.yaml index 82c1b3d..ae52bd9 100644 --- a/Ticket_Allocation.swagger.yaml +++ b/Ticket_Allocation.swagger.yaml @@ -4,7 +4,7 @@ info: version: 1.0.0 contact: {} servers: - - url: localhost + - url: http://localhost:3000 paths: /ticket_options/{ticketOptionID}: get: @@ -13,7 +13,9 @@ paths: operationId: getTicketOption responses: '200': - description: '' + description: Ticket option found + '404': + description: Ticket option not found parameters: - name: ticketOptionID in: path @@ -27,7 +29,7 @@ paths: operationId: createTicketOption requestBody: content: - application/json: + application/vnd.api+json: schema: type: object properties: @@ -38,7 +40,8 @@ paths: type: object properties: allocation: - type: number + type: integer + minimum: 0 example: 100 description: type: string @@ -59,8 +62,10 @@ paths: name: Test ticket option type: ticket_options responses: - '200': - description: '' + '201': + description: Ticket option created + '400': + description: Invalid request body /purchases: post: summary: Create Purchase @@ -70,7 +75,7 @@ paths: operationId: createPurchase requestBody: content: - application/json: + application/vnd.api+json: schema: type: object properties: @@ -81,7 +86,8 @@ paths: type: object properties: quantity: - type: number + type: integer + minimum: 1 example: 1 relationships: type: object @@ -130,6 +136,12 @@ paths: type: users type: purchases responses: - '200': - description: '' + '201': + description: Purchase created + '400': + description: >- + Invalid purchase (e.g. quantity exceeds remaining allocation or is + not a positive integer); no tickets are persisted + '404': + description: Referenced ticket option not found tags: []