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
6 changes: 3 additions & 3 deletions app/controllers/loans_controller.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
class LoansController < ActionController::API

rescue_from ActiveRecord::RecordNotFound do |exception|
render json: 'not_found', status: :not_found
render json: { error: 'not_found' }, status: :not_found
end

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
34 changes: 34 additions & 0 deletions app/controllers/payments_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
class PaymentsController < ActionController::API
before_action :set_loan

rescue_from ActiveRecord::RecordNotFound do |exception|
render json: { error: 'not_found' }, status: :not_found
end

def index
render json: @loan.payments
end

def show
render json: @loan.payments.find(params[:id])
end

def create
@payment = @loan.payments.build(payment_params)
if @payment.save
render json: @payment, status: :created
else
render json: { errors: @payment.errors.full_messages }, status: :unprocessable_entity
end
end

private

def set_loan
@loan = Loan.find(params[:loan_id])
end

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

# Calculates the outstanding balance: funded_amount minus total payments made
def outstanding_balance
funded_amount - payments.sum(:amount)
end
end
16 changes: 16 additions & 0 deletions app/models/payment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Payment < ActiveRecord::Base
belongs_to :loan

validates :payment_date, presence: true
validates :amount, presence: true, numericality: { greater_than: 0 }
validate :amount_does_not_exceed_outstanding_balance

private

def amount_does_not_exceed_outstanding_balance
existing_total = loan.payments.where.not(id: self.id).sum(:amount)
if loan && amount && (existing_total + amount > loan.funded_amount)
errors.add(:amount, "exceeds the outstanding balance of the loan")
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, only: [:index, :show], defaults: { format: :json } do
resources :payments, only: [:index, :show, :create], defaults: { format: :json }
end
end
11 changes: 11 additions & 0 deletions db/migrate/20250313061519_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, null: false, foreign_key: true
t.date :payment_date, null: false
t.decimal :amount, precision: 15, scale: 2, null: false

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_03_13_061519) 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", null: false
t.date "payment_date", null: false
t.decimal "amount", precision: 15, scale: 2, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["loan_id"], name: "index_payments_on_loan_id"
end

end
35 changes: 23 additions & 12 deletions spec/controllers/loans_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,36 @@
require 'rails_helper'

RSpec.describe LoansController, type: :controller do
describe '#index' do
it 'responds with a 200' do
get :index
let!(:loan) { Loan.create!(funded_amount: 1000) }
let!(:payment) { Payment.create!(loan: loan, payment_date: Date.today, amount: 200) }

describe "GET #index" do
it "returns loans with the outstanding balance included" do
get :index, format: :json
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json.first).to have_key("outstanding_balance")
expect(json.first["outstanding_balance"].to_f).to eq(800.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)
describe "GET #show" do
context "with a valid id" do
it "returns a loan with its outstanding balance" do
get :show, params: { id: loan.id }, format: :json
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json).to have_key("outstanding_balance")
expect(json["outstanding_balance"].to_f).to eq(800.0)
end
end

context 'if the loan is not found' do
it 'responds with a 404' do
get :show, params: { id: 10000 }
context "with an invalid id" do
it "returns a 404 not_found response" do
get :show, params: { id: 0 }, format: :json
expect(response).to have_http_status(:not_found)
json = JSON.parse(response.body)
expect(json["error"]).to eq("not_found")
end
end
end
Expand Down
44 changes: 44 additions & 0 deletions spec/controllers/payments_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'rails_helper'

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

describe "GET #index" do
it "returns a list of payments for a loan" do
get :index, params: { loan_id: loan.id }, format: :json
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json.size).to eq(1)
end
end

describe "GET #show" do
it "returns a specific payment" do
get :show, params: { loan_id: loan.id, id: payment.id }, format: :json
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json["id"]).to eq(payment.id)
end
end

describe "POST #create" do
context "with valid attributes" do
it "creates a new payment" do
post :create, params: { loan_id: loan.id, payment: { payment_date: '2025-03-13', amount: 500 } }, format: :json
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json["amount"].to_f).to eq(500.0)
end
end

context "with invalid attributes" do
it "returns errors when payment exceeds the outstanding balance" do
post :create, params: { loan_id: loan.id, payment: { payment_date: '2025-03-13', amount: 900 } }, format: :json
expect(response).to have_http_status(:unprocessable_entity)
json = JSON.parse(response.body)
expect(json["errors"]).to include("Amount exceeds the outstanding balance of the loan")
end
end
end
end
21 changes: 21 additions & 0 deletions spec/models/payment_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'rails_helper'

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

it "is valid with a payment amount within the outstanding balance" do
payment = Payment.new(loan: loan, payment_date: Date.today, amount: 4500)
expect(payment).to be_valid
end

it "is invalid if the payment amount exceeds the outstanding balance" do

Payment.create!(loan: loan, payment_date: Date.today, amount: 5000)
Payment.create!(loan: loan, payment_date: Date.today, amount: 4000)
Payment.create!(loan: loan, payment_date: Date.today, amount: 800)

payment = Payment.new(loan: loan, payment_date: Date.today, amount: 300)
expect(payment).not_to be_valid
expect(payment.errors[:amount]).to include("exceeds the outstanding balance of the loan")
end
end