Skip to content

Commit

Permalink
Merge pull request #4 from martinyonatann/feat/loan-system-design
Browse files Browse the repository at this point in the history
add loan-system-design
  • Loading branch information
martinyonatann authored Aug 16, 2024
2 parents d7a16bf + 69265ab commit 4201671
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 32 deletions.
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,16 @@ setup:
@docker-compose -f ./infrastructure/docker-compose.local.yml up -d
@sleep 8

## down: Set down database temporary for local environtment
.PHONY: down
down:
@echo "make down ${IS_IN_PROGRESS}"
@docker-compose -f ./infrastructure/docker-compose.local.yml down -t 1


.PHONY: migrate-up
migrate-up:
@goose -dir databases/migrations mysql "${DB_USER}:${DB_PASS}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}" up
@goose -dir databases/migrations mysql "${DB_USER}:${DB_PASS}@tcp(${DB_HOST}:${DB_PORT})/${DB_NAME}" up

PHONY: migrate-down
migrate-down:
Expand Down
61 changes: 45 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# ACTE
# ACTE (Axxxxx Code Test Engineering)

![flow-design](internal/loans/architecture-design.png)


# To-Do :
- Proposed Code Problems
- [x] Billing Engine (System Design and abstraction)
- [x] reconciliation service (Algorithmic and scaling)
- [ ] Loan Service (system design and abstraction)
- [x] Billing Engine (System Design and abstraction) [Billings Domain](internal/billings)
- [x] reconciliation service (Algorithmic and scaling) [Reconciliations Domain](internal/reconciliations)
- [x] Loan Service (system design and abstraction) [System Design Documentation](internal/loans/README.md)
- [x] Postman with Example Response

## Technology Overview
Expand All @@ -15,21 +17,48 @@
- **Docker**: Containerization platform
- **Viper**: Configuration management tool

## Project Details
## How To Run

### Prerequisites

Before running the application, ensure you have the following installed:

- **Docker**: Used for containerization. [Install Docker](https://docs.docker.com/get-docker/)
- **Go**: Programming language used for the project. [Install Go](https://golang.org/doc/install)
- **Goose**: Database migration tool used for managing database schema. Install it using Go:

```sh
go install github.com/pressly/goose/v3@latest
```

### Running the Application

**1. Clone the Repository**

```sh
git clone https://github.com/martinyonatann/acte.git
cd acte
```

**2. Build and Run Database on Docker**
```sh
make setup
```

### Billing Engine for Loan Management
The billing engine manages and tracks loans, providing features such as loan schedules, outstanding balances, and delinquency status for a 50-week loan with a 10% annual interest rate.
**3. Run Database Migrations**

### Reconciliation Service
The reconciliation service identifies unmatched and discrepant transactions between internal data (system transactions) and external data (bank statements).
Use Goose to apply database migrations. You may need to configure Goose to point to your database.

### Loan Service
The loan service will be designed to manage loan-related operations, including loan creation, payment processing, and tracking.
```sh
make migrate-up
```
**4. Start the Application**

### Postman with Example Response
Postman is used for testing the API endpoints with example responses to ensure correct functionality and integration.
use the following Go command to start the application:
```sh
go run main.go
```

### Configuration Management
- **Viper**: Used for managing configuration settings across different environments.
**5. import Postman collection file into Postman**

For detailed implementation and documentation, refer to the respective sections and code files in the project repository.
import file `acte.postman_collection.json` into your postman
16 changes: 8 additions & 8 deletions acte.postman_collection.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE loans ADD borrower_id int NULL AFTER id;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE loans DROP COLUMN borrower_id;
-- +goose StatementEnd
18 changes: 18 additions & 0 deletions databases/migrations/20240817005306_add_loan_states_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE `loan_states` (
id INT AUTO_INCREMENT PRIMARY KEY,
borrower_id VARCHAR(255) NOT NULL,
principal_amount DECIMAL(15, 2) NOT NULL,
rate DECIMAL(5, 2) NOT NULL,
roi DECIMAL(5, 2) NOT NULL,
state ENUM('proposed', 'approved', 'invested', 'disbursed') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE `loan_states`;
-- +goose StatementEnd
16 changes: 16 additions & 0 deletions databases/migrations/20240817005431_add_loan_approvals_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE `loan_approvals` (
id INT AUTO_INCREMENT PRIMARY KEY,
loan_state_id INT NOT NULL,
employee_id INT NOT NULL,
proof_path TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE `loan_approvals`;
-- +goose StatementEnd
17 changes: 17 additions & 0 deletions databases/migrations/20240817005802_add_investments_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE `investments` (
id INT AUTO_INCREMENT PRIMARY KEY,
loan_state_id INT NOT NULL,
investor_id INT NOT NULL,
investor_agreement_letter_path TEXT NOT NULL,
amount DECIMAL(15, 2) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE `investments`;
-- +goose StatementEnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE loan_disbursements (
id INT AUTO_INCREMENT PRIMARY KEY,
loan_state_id INT NOT NULL,
borrower_agreement_letter_path TEXT NOT NULL,
employee_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE `loan_disbursements`;
-- +goose StatementEnd
5 changes: 3 additions & 2 deletions internal/billings/delivery/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ func (h *BillingHandler) CreateLoan(c echo.Context) error {
return response.ErrorBuilder(app_error.BadRequest(err)).Send(c)
}

if err := h.uc.CreateLoan(c.Request().Context(), request); err != nil {
loanData, err := h.uc.CreateLoan(c.Request().Context(), request)
if err != nil {
return response.ErrorBuilder(err).Send(c)
}

return response.SuccessBuilder(nil).Send(c)
return response.SuccessBuilder(loanData).Send(c)
}

func (h *BillingHandler) DetailLoan(c echo.Context) error {
Expand Down
7 changes: 7 additions & 0 deletions internal/billings/dtos/loans.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

type Loan struct {
ID int64 `json:"id"`
BorrowerID int64 `json:"borrower_id"`
Amount float64 `json:"amount"`
Outstanding float64 `json:"outstanding"`
InterestRate float64 `json:"interest_rate"`
Expand All @@ -17,10 +18,15 @@ type Loan struct {
}

type CreateLoanRequest struct {
BorrowerID int64 `json:"borrower_id"`
Amount float64 `json:"amount"`
TermOfPaymentInWeek int `json:"term_of_payment_in_week"`
}

type CreateLoanResponse struct {
ID int64 `json:"id"`
}

func (c CreateLoanRequest) Validate() error {
return validation.ValidateStruct(&c,
validation.Field(&c.Amount, validation.Required),
Expand All @@ -30,6 +36,7 @@ func (c CreateLoanRequest) Validate() error {

type DetailLoan struct {
ID int64 `json:"id"`
BorrowerID int64 `json:"borrower_id"`
Amount float64 `json:"amount"`
Outstanding float64 `json:"outstanding"`
InterestRate float64 `json:"interest_rate"`
Expand Down
6 changes: 6 additions & 0 deletions internal/billings/entities/loans.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (

type Loan struct {
ID int64 `gorm:"column:id;primaryKey"`
BorrowerID int64 `gorm:"column:borrower_id"`
Amount float64 `gorm:"column:amount"`
Outstanding float64 `gorm:"column:outstanding"`
InterestRate float64 `gorm:"column:interest_rate"`
Expand All @@ -30,6 +31,7 @@ type DelinquentLoans []DelinquentLoan

type UpdateLoan struct {
ID int64 `gorm:"column:id;primaryKey"`
BorrowerID *int64 `gorm:"column:borrower_id"`
Amount *float64 `gorm:"column:amount"`
Outstanding *float64 `gorm:"column:outstanding"`
InterestRate *float64 `gorm:"column:interest_rate"`
Expand All @@ -43,6 +45,7 @@ func (Loan) TableName() string {

func NewLoan(request dtos.CreateLoanRequest) *Loan {
return &Loan{
BorrowerID: request.BorrowerID,
Amount: request.Amount,
InterestRate: InterestRate,
TermOfPaymentInWeek: request.TermOfPaymentInWeek,
Expand All @@ -53,6 +56,7 @@ func NewLoan(request dtos.CreateLoanRequest) *Loan {

type DetailLoan struct {
ID int64 `gorm:"column:id;primaryKey"`
BorrowerID int64 `gorm:"column:borrower_id"`
Amount float64 `gorm:"column:amount"`
Outstanding float64 `gorm:"column:outstanding"`
InterestRate float64 `gorm:"column:interest_rate"`
Expand Down Expand Up @@ -95,6 +99,7 @@ func MapDetailLoan(l DetailLoan) dtos.DetailLoan {

return dtos.DetailLoan{
ID: l.ID,
BorrowerID: l.BorrowerID,
Amount: l.Amount,
Outstanding: l.Outstanding,
InterestRate: l.InterestRate,
Expand All @@ -114,6 +119,7 @@ func MapDelinquentLoans(loans DelinquentLoans) dtos.DelinquentLoans {
UnpaidWeeks: loan.UnpaidWeeks,
Loan: dtos.Loan{
ID: loan.ID,
BorrowerID: loan.BorrowerID,
Amount: loan.Amount,
Outstanding: loan.Outstanding,
InterestRate: InterestRate,
Expand Down
2 changes: 1 addition & 1 deletion internal/billings/repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (r *billingRepo) GetRepaymentAmount(ctx context.Context, loanID int64, repa
func (r *billingRepo) ListDelinquentLoan(ctx context.Context, params *pagination.PaginationRequest) (loans entities.DelinquentLoans, err error) {
query := r.db.WithContext(ctx).
Table("loans l").
Select("l.id, l.amount, l.outstanding, l.term_of_payment_in_week, l.created_at, l.updated_at, COUNT(*) AS unpaid_weeks").
Select("l.id,l.borrower_id, l.amount, l.outstanding, l.term_of_payment_in_week, l.created_at, l.updated_at, COUNT(*) AS unpaid_weeks").
Joins("JOIN loan_schedules ls ON ls.loan_id = l.id").
Where("ls.is_paid IS NOT TRUE AND ls.schedule_date <= DATE(?)", time.Now().Format(time.DateOnly)).
Group("l.id").
Expand Down
2 changes: 1 addition & 1 deletion internal/billings/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

type BillingUC interface {
CreateLoan(ctx context.Context, request dtos.CreateLoanRequest) error
CreateLoan(ctx context.Context, request dtos.CreateLoanRequest) (response dtos.CreateLoanResponse, err error)
DetailLoan(ctx context.Context, loanID int64) (loan dtos.DetailLoan, err error)
LoanRepayment(ctx context.Context, request dtos.LoanRepaymentRequest) error
InquireRepaymentAmount(ctx context.Context, request dtos.InquireRepaymentAmountRequest) (inquiryRepayment dtos.InquireRepaymentAmountResponse, err error)
Expand Down
12 changes: 9 additions & 3 deletions internal/billings/usecase/usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,21 @@ func NewBillingUC(repo billings.BillingRepository) billings.BillingUC {
return &billingUC{repo: repo}
}

func (uc *billingUC) CreateLoan(ctx context.Context, request dtos.CreateLoanRequest) error {
return uc.repo.Atomic(ctx, &sql.TxOptions{}, func(tx billings.BillingRepository) error {
loanData := entities.NewLoan(request)
func (uc *billingUC) CreateLoan(ctx context.Context, request dtos.CreateLoanRequest) (response dtos.CreateLoanResponse, err error) {
loanData := entities.NewLoan(request)
err = uc.repo.Atomic(ctx, &sql.TxOptions{}, func(tx billings.BillingRepository) error {
if err := tx.CreateLoan(ctx, loanData); err != nil {
return err
}

return tx.CreateLoanSchedules(ctx, entities.NewLoanSchedules(request, *loanData))
})

if err != nil {
return response, err
}

return dtos.CreateLoanResponse{ID: loanData.ID}, nil
}

func (uc *billingUC) DetailLoan(ctx context.Context, loanID int64) (loan dtos.DetailLoan, err error) {
Expand Down
Loading

0 comments on commit 4201671

Please sign in to comment.