Giter Site home page Giter Site logo

backend-engineer-assignment's Introduction

Project: Receipt Processor Service

codecov

  • Build a webservice that fulfils the documented API. The API is described below. A formal definition is provided in the api.yml file
  • Data does not need to persist when your application stops. It is sufficient to store information in memory.

These rules collectively define how many points should be awarded to a receipt.

  • One point for every alphanumeric character in the retailer name.
  • 50 points if the total is a round dollar amount with no cents.
  • 25 points if the total is a multiple of 0.25.
  • 5 points for every two items on the receipt.
  • If the trimmed length of the item description is a multiple of 3, multiply the price by 0.2 and round up to the nearest integer. The result is the number of points earned.
  • 6 points if the day in the purchase date is odd.
  • 10 points if the time of purchase is after 2:00pm and before 4:00pm.

Examples

{
  "retailer": "Target",
  "purchaseDate": "2022-01-01",
  "purchaseTime": "13:01",
  "items": [
    {
      "shortDescription": "Mountain Dew 12PK",
      "price": "6.49"
    },{
      "shortDescription": "Emils Cheese Pizza",
      "price": "12.25"
    },{
      "shortDescription": "Knorr Creamy Chicken",
      "price": "1.26"
    },{
      "shortDescription": "Doritos Nacho Cheese",
      "price": "3.35"
    },{
      "shortDescription": "   Klarbrunn 12-PK 12 FL OZ  ",
      "price": "12.00"
    }
  ],
  "total": "35.35"
}
Total Points: 28
Breakdown:
     6 points - retailer name has 6 characters
    10 points - 4 items (2 pairs @ 5 points each)
     3 Points - "Emils Cheese Pizza" is 18 characters (a multiple of 3)
                item price of 12.25 * 0.2 = 2.45, rounded up is 3 points
     3 Points - "Klarbrunn 12-PK 12 FL OZ" is 24 characters (a multiple of 3)
                item price of 12.00 * 0.2 = 2.4, rounded up is 3 points
     6 points - purchase day is odd
  + ---------
  = 28 points
{
  "retailer": "M&M Corner Market",
  "purchaseDate": "2022-03-20",
  "purchaseTime": "14:33",
  "items": [
    {
      "shortDescription": "Gatorade",
      "price": "2.25"
    },{
      "shortDescription": "Gatorade",
      "price": "2.25"
    },{
      "shortDescription": "Gatorade",
      "price": "2.25"
    },{
      "shortDescription": "Gatorade",
      "price": "2.25"
    }
  ],
  "total": "9.00"
}
Total Points: 109
Breakdown:
    50 points - total is a round dollar amount
    25 points - total is a multiple of 0.25
    14 points - retailer name (M&M Corner Market) has 14 alphanumeric characters
                note: '&' is not alphanumeric
    10 points - 2:33pm is between 2:00pm and 4:00pm
    10 points - 4 items (2 pairs @ 5 points each)
  + ---------
  = 109 points

Notes

  • Design Approach:
    • To meet the requirement that data need not persist across server restarts, I utilized an in-memory map in Go to store receipts. This approach avoids the overhead of setting up a database like PostgreSQL.
    • However, since Go maps are not concurrency-safe, I implemented a mutex lock rs.mu.Lock() to ensure safe concurrent writes to the inMemoryReceiptMap and rs.mu.RLock() to ensure safe concurrent read access to the inMemoryReceiptMap helping for read heavy application. The lock is acquired before performing get/put operations and released afterward using defer rs.mu.Unlock() or defer rs.mu.RUnlock(). Here whenever rs.mu.Lock() is acquired there is no concurrent reads can happen as an exclusive lock
      // Get retrieves a receipt from the in-memory store by its ID.
      // It returns a ReceiptGetResponse containing the points if the receipt is found,
      // otherwise, it returns an error indicating that the receipt was not found.
      func (rs *receiptStore) Get(receiptID string) (*model.ReceiptGetResponse, error) {
          rs.mu.RLock()         // Locks the receipt store to ensure thread-safe access.
          defer rs.mu.RUnlock() // Unlocks the receipt store after the operation.
    
          receipt, exists := rs.inMemoryReceiptMap[receiptID]
          ...... 
      }
      // Insert adds a new receipt to the in-memory store.
      // It returns a ReceiptPostResponse containing the ID of the newly inserted receipt.
      func (rs *receiptStore) Insert(receipt *model.Receipt) *model.ReceiptPostResponse {
          rs.mu.Lock()
          defer rs.mu.Unlock()
    
          rs.inMemoryReceiptMap[receipt.Id] = *receipt
      	.......
      }
  • Code Structure: The codebase is well-structured and clean, organized into different packages. It uses a layered architecture with the following components:
    • Handler Layer: Handles requests and validates request parameters.
    • Service Layer: Contains the business logic.
    • Data Layer: Manages data operations.
    • Each layer interacts with the interfaces of the next layer, and dependency injection is used to inject objects from one layer into another, achieving inversion of control.
  • Scalability Considerations: While this solution works for the given requirements, it does have limitations regarding horizontal scalability. Running multiple instances of the application would lead to each instance having its own in-memory store, resulting in data integrity issues due to the lack of a single source of truth.
  • Reflection: While this layered architecture may seem like over-engineering for a simple task, but it is designed with future code scalability in mind. Additionally, a logger and custom error messages are implemented to enhance robustness and maintainability. Other than that Working on this assignment was an enjoyable experience. It provided a great opportunity to refresh my knowledge of best production code practices and reinforced the importance of a layered architecture in application development.

How to run

Installation

1. Install docker on the desktop - https://docs.docker.com/compose/install/
2. Check for docker & docker-compose version -  `docker --version`, `docker-compose --version`

Running the project

  1. Clone the project

git clone [email protected]:shivasaicharanruthala/backend-engineer-assignment.git

  1. Go project directory

cd backend-engineer-assignment

  1. Run the docker-compose file

docker-compose up -d

  1. Check the logs of application container
  - check for active logs: `docker ps`
  - check for container id of the image: `docker ps --filter "ancestor=shiva5128/backend-engineer-assignment:latest"`
  - check for logs: `docker logs <container-id>`
  
  Logs are of these format 
  2024/06/25 03:42:16 INFO: {"level":"INFO","msg":"Logger initialized successfully"}
  2024/06/25 03:42:16 INFO: {"level":"INFO","msg":"Receipts Server starting to listen on port 8080"}

How to test

Using CuRL

  1. Endpoint: Process Receipts
    • Path: /v1/receipts/process
    • Method: POST
curl --location --request POST 'http://localhost:8080/v1/receipts/process' \
--header 'Content-Type: application/json' \
--data '{
  "retailer": "Target",
  "purchaseDate": "2022-01-01",
  "purchaseTime": "13:01",
  "items": [
    {
      "shortDescription": "Mountain Dew 12PK",
      "price": "6.49"
    },
    {
      "shortDescription": "Emils Cheese Pizza",
      "price": "12.25"
    },
    {
      "shortDescription": "Knorr Creamy Chicken",
      "price": "1.26"
    },
    {
      "shortDescription": "Doritos Nacho Cheese",
      "price": "3.35"
    },
    {
      "shortDescription": "   Klarbrunn 12-PK 12 FL OZ  ",
      "price": "12.00"
    }
  ],
  "total": "35.35"
}' -i -v
  1. verify the response sent back after storing the receipt
{"id": "370fc237-9d4c-4d2f-a056-023080c755e2"}
  1. Endpoint: Get Points
    • Path: /v1/receipts//{id}/points
    • Method: GET
  • Replace {id} in path http://localhost:8080/v1/receipts/{id}/points with the uuid seen in step 2.
  • execute below GET CuRL
curl -v -X GET 'http://localhost:8080/v1/receipts/{id}/points' -i -v
  1. Verify the response got back after executing GET CuRL
{ "points" : "28" }

Using Postman

postman_testing.gif

Using Unit & Integration tests

  • You can go through individual test cases of a function in each package or to run all test cases in one go execute below command

go test -v ./...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.