Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
class ApplicationController < ActionController::Base
class ApplicationController < ActionController::API
rescue_from ActiveRecord::RecordNotFound do |exception|
render json: 'not_found', status: :not_found
end
end
10 changes: 3 additions & 7 deletions app/controllers/loans_controller.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
class LoansController < ActionController::API

rescue_from ActiveRecord::RecordNotFound do |exception|
render json: 'not_found', status: :not_found
end
class LoansController < ApplicationController

def index
render json: Loan.all
render json: Loan.all.as_json(methods: :outstanding_balance)
end

def show
render json: Loan.find(params[:id])
render json: Loan.find(params[:id]).as_json(methods: :outstanding_balance)
end
end
26 changes: 26 additions & 0 deletions app/controllers/payments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class PaymentsController < ApplicationController
def index
render json: Loan.find(params[:loan_id]).payments
end

def show
render json: Loan.find(params[:loan_id]).payments.find(params[:id])
end

def create
loan = Loan.find(params[:loan_id])
payment = loan.payments.build(payment_params)

if payment.save
render json: payment, status: :created
else
render json: payment.errors, status: :unprocessable_entity
end
end

private

def payment_params
params.require(:payment).permit(:date, :amount)
end
end
5 changes: 5 additions & 0 deletions app/models/loan.rb
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
class Loan < ActiveRecord::Base
has_many :payments

def outstanding_balance
funded_amount - payments.sum(:amount)
end
end
18 changes: 18 additions & 0 deletions app/models/payment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Payment < ActiveRecord::Base
belongs_to :loan

before_create :validate_and_create_payment

private

def validate_and_create_payment
ActiveRecord::Base.transaction do
loan.with_lock do
if amount > loan.outstanding_balance
errors.add(:amount, "Payment amount exceeds loan balance")
throw :abort
end
end
end
end
end
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
Rails.application.routes.draw do
resources :loans, defaults: {format: :json}
resources :loans, defaults: {format: :json}, only: [:index, :show] do
resources :payments, defaults: {format: :json}, only: [:index, :show, :create]
end
end
11 changes: 11 additions & 0 deletions db/migrate/20250226204247_create_payments.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class CreatePayments < ActiveRecord::Migration[5.2]
def change
create_table :payments do |t|
t.references :loan, foreign_key: true
t.date :date
t.decimal :amount, precision: 8, scale: 2

t.timestamps
end
end
end
11 changes: 10 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,21 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema.define(version: 20150903195334) do
ActiveRecord::Schema.define(version: 2025_02_26_204247) do

create_table "loans", force: :cascade do |t|
t.decimal "funded_amount", precision: 8, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end

create_table "payments", force: :cascade do |t|
t.integer "loan_id"
t.date "date"
t.decimal "amount", precision: 8, scale: 2
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["loan_id"], name: "index_payments_on_loan_id"
end

end
17 changes: 15 additions & 2 deletions spec/controllers/loans_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
require 'rails_helper'

RSpec.describe LoansController, type: :controller do
let(:loan) { Loan.create!(funded_amount: 100.0) }

describe '#index' do
it 'responds with a 200' do
get :index
expect(response).to have_http_status(:ok)
end

it 'includes oustanding balance in the response' do
loan # create loan
get :index
json_response = JSON.parse(response.body)
expect(json_response.first['outstanding_balance'].to_f).to be 100.0
end
end

describe '#show' do
let(:loan) { Loan.create!(funded_amount: 100.0) }

it 'responds with a 200' do
get :show, params: { id: loan.id }
expect(response).to have_http_status(:ok)
end

it 'includes oustanding balance in the response' do
get :show, params: { id: loan.id }
json_response = JSON.parse(response.body)
expect(json_response['outstanding_balance'].to_f).to be 100.0
end

context 'if the loan is not found' do
it 'responds with a 404' do
get :show, params: { id: 10000 }
Expand Down
48 changes: 48 additions & 0 deletions spec/controllers/payments_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require 'rails_helper'

RSpec.describe PaymentsController, type: :controller do
let(:loan) { Loan.create!(funded_amount: 100.0) }
let(:payment) { Payment.create!(loan: loan, date: Date.today, amount: 100.00) }

describe "#index" do
it "returns http success with payments" do
payment
get :index, params: { loan_id: loan.id }
expect(response).to have_http_status(:success)
expect(response.body).to include("100.0")
end

context "when loan is not found" do
it "returns http not found" do
get :index, params: { loan_id: 10000 }
expect(response).to have_http_status(:not_found)
end
end
end

describe "#show" do
it "returns http success with payment details" do
payment
get :show, params: { loan_id: loan.id, id: payment.id }
expect(response).to have_http_status(:success)
expect(response.body).to include("100.0")
end
end

describe "#create" do
it "add payment & returns http success" do
expect do
post :create, params: { loan_id: loan.id, payment: { date: Date.today, amount: 100.00 } }
expect(response).to have_http_status(:success)
end.to change { loan.payments.count }.by(1)
end

it "returns http unprocessable_entity when payment exeeds loan balance" do
expect do
post :create, params: { loan_id: loan.id, payment: { date: Date.today, amount: 1000.00 } }
expect(response).to have_http_status(:unprocessable_entity)
expect(response.body).to include("Payment amount exceeds loan balance")
end.to_not change { loan.payments.count }
end
end
end
20 changes: 20 additions & 0 deletions spec/models/payment_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'rails_helper'

RSpec.describe Payment, type: :model do
let(:loan) { Loan.create!(funded_amount: 100.0) }

it "is thread-safe" do
payment1 = Payment.new(loan: loan, date: Date.today, amount: 60.0)
payment2 = Payment.new(loan: loan, date: Date.today, amount: 60.0)

thread1 = Thread.new { payment1.save }
thread2 = Thread.new { payment2.save }

thread1.join
thread2.join

loan.reload
expect(loan.outstanding_balance).to eq(40.0)
expect(loan.payments.count).to eq(1)
end
end