Skip to content

mikibakaiki/processing-payment-challenge

Repository files navigation

Laravel Logo

Laravel Payment Processing

This project implements a payment system that processes amortizations and payments, ensuring that payments are correctly handled.

Assumptions

  • You have PHP and composer installed on your system
  • You have Docker and docker-compose installed on your system. Check Laravel's instructions for each OS
  • You have nodejs and npm installed on your system.
  • The project wallet can never be negative.
  • The date that will be checked to see if an Amortization is overdue is being calculated at runtime and will be the current date and time of the execution.
  • An Amortization can only have two states: pending and paid.
  • An amortization is considered paid only when its corresponding payment is successfully processed, and the amortization's state is updated from pending to paid.
  • The database will always have data, before initializing the frontend page.

Setup

Installing Dependencies

First, we need to install the PHP dependencies.

composer install

⚠️ NOTE: if this command does not work - for me it didn't, at first - run the following command to install some possible missing dependencies:

sudo apt-get install php8.1-xml php8.1-curl

Then, we need to generate an APP_KEY, which will be used on the Env Variables

php artisan key:generate

Env Variables

Please create your .env file before following with the setup. You can use the .env.example as a starting point.

These were the variables that I've set:

APP_NAME=Laravel
APP_ENV=local
APP_KEY=<use here the generated key>
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=processing_payment_challenge
DB_USERNAME=sail
DB_PASSWORD=<some_password>

QUEUE_CONNECTION=redis

BROADCAST_DRIVER=log
CACHE_DRIVER=redis
FILESYSTEM_DISK=local
SESSION_DRIVER=redis
SESSION_LIFETIME=120

REDIS_CLIENT=predis
REDIS_HOST=redis
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_ENCRYPTION=null

Setting Up The Project

This project is built with Sail, a lightweight command-line interface for interacting with Laravel's default Docker development environment.

Sail is a dependency of Laravel, as is installed in the vendor/bin directory. Therefore, to execute it, we would have to write a lot. So we create an alias to execute it with the command sail

Run this command to start:

alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'

Now we are ready to bring our containers up. Run this command to initialize the project. It might take some time to download everything.

sail up

To start all containers in the background, run:

sail up -d

Now we want to create the tables.

The migrate:fresh command will drop all tables from the database and then execute the migrate command. The --seed command will use the seed data.

sail artisan migrate:fresh --seed

Running the Queue

To handle all the payment processing, we're using a Queue. This allows for each API call to be super fast. Therefore, if there are millions of users calling our API, we can handle all the requests efficiently: the queue will process all the jobs on the background, asynchronously.

To have our qeueu running and able to process jobs, run this command:

sail artisan queue:work redis

If there are any jobs, the queue will process them. If there are no more jobs, the queue will still be listening and waiting for more jobs to come.

If you want to start the queue with a limit to the number of jobs it should run - it may be useful for testing and to see what is actually going on - use:

sail artisan queue:work redis --max-jobs=100

⚠️ NOTE: I had memory issues when trying to run my queue - I'm using Ubuntu 22.04 in WSL2. If you find the same problem, run this command:

sysctl vm.overcommit_memory=1

It sets the memory overcommit behavior to allow all memory allocations, regardless of the current status of memory.

For more Queue options, visit Laravel's official documentation

Shutting Down

If you have the Queue running, don't forget to terminate it using Ctrl + C.

To shut down, simply run the command:

sail stop

If you want to start off with fresh containers, with a clean cache, and keeping all data, run:

sail down

API

After you Setup everything, you can fully use the software.

Pay All Due Amortizations

In this endpoint, we issue the command for the server to start processing the amortizations. This endpoint returns a batch_id.

Here's a curl request to use it:

curl --location 'http://localhost:80/api/v1/pay-all-due-amortizations'

Check Batch Status

This endpoint allows us to check the status of the batch with a specific id. To get a valid id, check the Pay All Due Amortizations. This endpoint returns an object with data on the jobs executing for that specific batch.

Here's a curl request to use it:

curl --location 'http://localhost:80/api/v1/check-batch-status/' \
--form 'batch_id="9a29a0cc-0076-4106-ae47-8cc8643ff29a"'

Get All Amortizations

This endpoint returns all the amortizations. It allows for four arguments:

  • page: the page number you want. The default is 1.
  • per_page: the number of elements per page. The default is 10.
  • order: the sorting order for a column. Can be asc or desc. The default is asc.
  • sort_by: the column on which to sort. The default is id.

This is the endpoint called by the Amortizations Page, served at http://localhost/amortizations

Here's a curl request to use it:

curl --location 'http://localhost/api/v1/index?page=3&per_page=50&order=desc&sort_by=schedule_date'

You can try all these requests on Hoppscotch.io!

Front End Webpage

Head to http://localhost:5173 and you can see the front end webpage.

We are able to:

  • See all the amortizations in the system
  • Sort each column in ascending or descending order
  • Set different page sizes
  • Check each page with pagination.

Database Relations

  • Each Amortization is associated with one Project.
  • Each Amortization can have multiple Payments associated with it.
  • Each Payment is associated with one Amortization.
  • Each Payment is associated with one Profile.
  • Each Payment is associated with one Promoter.
  • Each Profile can have multiple Payments associated with it.
  • Each Project can have multiple Amortizations associated with it.
  • Each Project is associated with one Promoter.
  • Each Promoter can have multiple Projects associated with it.
  • Each Promoter can have multiple Payments associated with it.
classDiagram
    Project "1" --> "*" Amortization : has
    Project "1" --> "1" Promoter : belongsTo
    Promoter "1" --> "*" Project : has
    Promoter "1" --> "*" Payment : has
    Amortization "1" --> "*" Payment : has
    Payment --> Amortization : belongsTo
    Payment --> Promoter : belongsTo
    Payment --> Profile : belongsTo

    class Project {
        +id: int
        +name: string
        +wallet_balance: double
    }

    class Promoter {
        +id: int
        +name: string
        +email: string
    }

    class Amortization {
        +id: int
        +schedule_date: datetime
        +state: string
        +amount: double
    }

    class Payment {
        +id: int
        +amount: double
        +state: string
    }

    class Profile {
        +id: int
        +name: string
        +email: string
    }
Loading

Containers

We have 8 containers in total:

  1. The server for the app
  2. MySql database
  3. PhpMyAdmin database UI, available at http://localhost:8080
  4. Mailpit, which allows us to view the email notifications that will be sent. Available at http://localhost:8025/
  5. Redis, which handles the queues.
  6. Redis Commander, a Redis UI. Available at http://localhost:6379/
  7. Vite, which is running the Vue app. Available at http://localhost:5173/
  8. Caddy, which allows for https with Laravel [OPTIONAL - was an experiment]

Basic Architecture and Performance

There are three main parts to this solution.

Controller

Handles the API requests and delegates computation to the service

Service

The service handles the necessary queries and dispatches jobs to execute in the background.

Job

The job handles the actual work of verifying if an amortization is due and if it can be successfully paid. It runs asynchronously, and each amortization has a dedicated job.

Performance

In this solution, we are using jobs, one for each Amortization to be processed, and another for each email notification to be sent. This allows for processing information asynchronously, on background processes, i.e., an API call can have a response in milliseconds, whilst the background process, that are in a Queue, can take minutes. For servers with millions of requests and users, this is a good approach.

For each request to the pay-all-due-amortizations endpoint, the service will create a Batch, which will aggregate jobs.

Then, it reads one Amortization at a time (taking advantage of cursor, which is more memory performant) and will create a Job.

When there are enough jobs, of a predetermined size (we are using 100) the service will push all the jobs into the Batch, taking advantage of Laravel's Job Batching.

This leverages the best of both worlds: reading only parts of the data at a time, which uses less memory, and using Job Batching, which allows for faster execution, reduces the overhead of dispatching individual jobs and the overhead of managing multiple batches, ensuring code consistency.

Inspiration