Membuat Simple Bank App Menggunakan Strapi v4
Published: 2023-04-02 07:47:52

Hi, selamat datang di artikel kami. Untuk kali ini kita akan sharing dan belajar bareng terkait [Strapi](https://strapi.io). Namun, sebelum kita mulai, ada beberapa hal yang mesti temen-temen pahami. 

Pertama, [Strapi](https://strapi.io) adalah *Headless Content Management System* atau *Headless CMS* yang dimana sangat membantu untuk mempercepet waktu _developer_ untuk membuat sebuah aplikasi.

Lalu apa itu *Headless Content Management System* atau *Headless CMS*? *Headless CMS* adalah sebuah CMS tanpa [Frontend](https://en.wikipedia.org/wiki/Front-end_web_development) yang dimana *content*-nya dapat diakses melalui [API](https://en.wikipedia.org/wiki/API). Sehingga dengan sebuah *Headless CMS* membantu temen-temen *developer* membuat backend aplikasi lebih cepat dan mudah.

[API](https://en.wikipedia.org/wiki/API) memungkinan dua aplikasi untuk saling berkomunikasi melalui sebuah *protocol* dengan *message contract* tertentu. Untuk pembahasan kali ini, kita akan menggunakan [HTTP](https://en.wikipedia.org/wiki/HTTP) dan [JSON](https://en.wikipedia.org/wiki/JSON)

[Strapi](https://strapi.io) biasanya digunakan para developer untuk mengembangkan *Static Websites*, *Mobile Apps*, *e-Commerce*, *Editorial*, dan *Corporate websites*. Namun, tidak menutup kemungkinan untuk mengembangankan aplikasi lebih dari itu. Untuk di artikel kami kali ini, kami akan coba membuat aplikasi bank yang sederhana atau **API Simple Bank App**.

Kami merasa dengan mempelajari **API Simple Bank App**, dapat membantu kita untuk mempelajari sistem perbankan dalam scope kecil dan manfaat dari API terutama di era digital saat ini. Adapun yang akan kita pelajari di artikel ini antara lain:

1. Business Requirements
2. Design System
3. Development

## Business Requirements

**API Simple Bank App** melayani user dalam beberapa hal antara lain:

1. User dapat mendaftarkan diri menjadi nasabah baru dan mendapatkan nomor *Customer Identification File* (CIF) dengan meng-input data antara lain:
   1. Nomor ID atau KTP
   2. Nama lengkap
2. Nasabah dapat membuat nomor rekening baru dengan menginput data CIF
3. Dapat melihat daftar nomor rekening dengan menginput data CIF
4. Nasabah dapat men-topup uang ke nomor rekeningnya sendiri
   1. Nomor Rekening
   2. *Amount*
5. Nasabah dapat menarik uang dari nomor rekeningnya sendiri dengan data antara lain:
   1. Nomor ID atau KTP
   2. Nomor Rekening
   3. *Amount*

## Design system

Berdasarkan Business Requirement di sebelumnya, kami melihat bahwa ada beberapa *service* yang perlu kita bangun di dalam sistem kita antara lain:

1. Customer service: service yang melayani nasabah dalam [CRUD](https://www.codecademy.com/article/what-is-crud) customer.

2. Account service: service yang melayani nasabah dalam [CRUD](https://www.codecademy.com/article/what-is-crud) account.

3. Transaction service: service yang melayani nasabah dalam [CRUD](https://www.codecademy.com/article/what-is-crud) transaction.

## Development

- Buka Terminal lalu jalankan perintah dibawah
```sh
npx create-strapi-app@latest app-simple-bank
```

- Masuk ke folder _app-simple-bank_ dengan menjalankan code di bawah
```sh
cd app-simple-bank
```

- Buat dua file yaitu _app.Dockerfile_, _docker-compose.yml_, dan _Makefile_

- Untuk _app.Dockerfile_, ketik kode di bawah ini
```dockerfile
FROM node:16-alpine

# Installing libvips-dev for sharp Compatibility
RUN apk update && apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev nasm bash vips-dev
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
WORKDIR /opt/
COPY ./package.json ./yarn.lock ./
ENV PATH /opt/node_modules/.bin:$PATH
RUN yarn config set network-timeout 600000 -g && yarn install
WORKDIR /opt/app
COPY ./ .
RUN yarn build
EXPOSE 1337
CMD ["yarn", "develop"]
```

- Untuk compose-docker.yml. ketik kode di bawah ini
```yaml
version: "3.7"
services:

  app-simple-bank:
    build:
      context: .
      dockerfile: ./app.Dockerfile
    container_name: app-simple-bank
    environment:
      - DATABASE_HOST=db-simple-bank
      - DATABASE_PORT=5432
      - DATABASE_NAME=postgres
      - DATABASE_USERNAME=postgres
      - DATABASE_PASSWORD=password
      - HOST=0.0.0.0
      - PORT=1337
      - APP_KEYS=mc68ZV26OzOPQ0A1ESUvNA==,b4sM7ksQE3eqfteYmqWzwA==,UUWwjcxNoDrGvSwnDDhwxA==,j7T57Bls1mSggOLIrsljkg==
      - API_TOKEN_SALT=nagThNX7aJINn6oaOpMOyg==
      - ADMIN_JWT_SECRET=9VrfUyP40Cqxy+6qNvucQQ==
      - TRANSFER_TOKEN_SALT=ar4K31sLK7NONBhHQ6t2zw==
    ports:
      - "8000:1337"
    depends_on:
      - db-simple-bank
    volumes:
      - ./src:/opt/app/src
      - ./config:/opt/app/config
      - ./package.json:/opt/app/package.json
      - ./public/uploads:/opt/app/public/uploads
    networks:
      app-net: {}

  db-simple-bank:
    image: postgres:alpine
    container_name: db-simple-bank
    environment:
      - POSTGRES_PASSWORD=password
    volumes:
      - ./data/pg-data:/var/lib/postgresql/data
    networks:
      app-net: {}

networks:
  app-net:
    external: true
    name: 'dev-to-network'
```
- Terakhir untuk _Makefile_, ketik kode di bawah ini
```Makefile
compose-up:
	docker compose up -d --build

compose-down:
	docker compose down
```

- Buka terminal di bawah, buat network baru di docker dengan menjalankan kode di bawah ini

```sh
docker network create dev-to-network
```

- Untuk menjalankan aplikasi nya, ketik kode di bawah ini di terminal
```sh
make compose-up
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3o9b4p52alxmtauuljo7.png)

- Buka browser, ketik di address bar http://localhost:8000/admin, maka tampilan akan seperti di bawah ini 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kqsgkj07pvmess5eajdk.png)

- Masukan data sesuai dengan yang diminta
- Setelah masuk ke halaman dashboard admin, pilih menu _Content-Type builder_
- Klik _Create new collection type_, lalu create _Customer_

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t2r8w0gfe04umbb75vl8.png)

- Create beberapa field seperti dibawah

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4pbzehl9lqvokihf9v6l.png)

- Klik _Create new collection type_, lalu create _Account_

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2y77y14okg2us4nsmatn.png)

- Create beberapa field seperti dibawah

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5cb6snv02yqp6pyquz7o.png)

- Klik Create new collection type, lalu create Transaction


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1v66xvf011kl65hfqqsj.png)

- Create beberapa field seperti dibawah

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yufl9b0g9a0fv4bvl312.png)

- Create _lifecycles.js_ file di _src/api/content-types/customer/lifecycles.js_, lalu ketikan kode di bawah ini
```javascript
// src/api/content-types/customer/lifecycles.js

module.exports = {
    beforeCreate: async (event) => {

        const generate_cif = async (length) => {
            let result = '';
            const characters = '0123456789';
            const charactersLength = characters.length;
            let counter = 0;
            while (counter < length) {
                result += characters.charAt(Math.floor(Math.random() * charactersLength));
                counter += 1;
            }
            return result;
        }

        const get_cif = async () => {
            
            let cif = ""
            let customer = []
            do {
                cif = await generate_cif(10)
                customer = await strapi.entityService.findMany("api::customer.customer", {
                    filters: {
                        cif: cif
                    }
                })
                console.log(`customer: ${customer}`)
                console.log(`cif: ${cif}`)
            } while (customer.length != 0)

            return cif
        }

        const { data } = event.params;
        data.cif = await get_cif()
    },
};
```

- Create _lifecycles.js_ file di _src/api/content-types/account/lifecycles.js_, lalu ketikan kode di bawah ini

```javascript
// src/api/content-types/account/lifecycles.js

const utils = require('@strapi/utils');
const { ApplicationError } = utils.errors;

module.exports = {
    beforeCreate: async (event) => {

        const generate_account_number = async (length) => {
            let result = '';
            const characters = '0123456789';
            const charactersLength = characters.length;
            let counter = 0;
            while (counter < length) {
                result += characters.charAt(Math.floor(Math.random() * charactersLength));
                counter += 1;
            }
            return result;
        }

        const get_account_number = async () => {

            let account_number = ""
            let account = []
            do {
                account_number = await generate_account_number(10)
                account = await strapi.entityService.findMany("api::account.account", {
                    filters: {
                        number: account_number
                    }
                })
                console.log(`account: ${account}`)
                console.log(`account_number: ${account_number}`)
            } while (account.length != 0)

            return account_number
        }

        const { data } = event.params;

        const customer = await strapi.entityService.findMany("api::customer.customer", {
            filters: {
                cif: data.cif_number
            }
        })

        if(customer.length == 0)
        {
            throw new ApplicationError('Invalid cif number', { message: `can not find '${data.cif_number}' cif number` }); 
        }
        else
        {
            data.number = await get_account_number()
            data.balance = 0
        }
    },
};
```

- Create file _custom-transaction.js_ di _src/api/transaction/controllers/custom-transaction.js_, lalu ketikan kode di bawah ini
```javascript
// src/api/transaction/controllers/custom-transaction.js

const utils = require('@strapi/utils');
const { ApplicationError } = utils.errors;

const get_account = async (account_number) => {
    const accountResponse = await strapi.entityService.findMany("api::account.account",
        {
            filters: {
                number: account_number
            }
        }
    )

    return accountResponse[0]
}

const generate_journal = async (length) => {
    let result = '';
    const characters = '0123456789';
    const charactersLength = characters.length;
    let counter = 0;
    while (counter < length) {
        result += characters.charAt(Math.floor(Math.random() * charactersLength));
        counter += 1;
    }
    return result;
}

const get_journal = async () => {

    let journal = ""
    let transaction = []
    do {
        journal = await generate_journal(10)
        transaction = await strapi.entityService.findMany("api::transaction.transaction", {
            filters: {
                journal: journal
            }
        })
        console.log(`journal: ${journal}`)
    } while (transaction.length != 0)

    return journal
}

module.exports = {
    async debit(ctx, next) {

        const requestBody = ctx.request.body.data
        const account = await get_account(requestBody.account_number)

        if (account.length == 0) {
            throw new ApplicationError('Invalid account number', { message: `Can not find ${requestBody.account_number} account number` });
        }
        else {
            const new_balance = account.balance - requestBody.amount
            const transaction = {
                data: {
                    journal: await get_journal(),
                    account_number: requestBody.account_number,
                    type: requestBody.type,
                    status: "SUCCEED",
                    action: "DEBIT",
                    amount: requestBody.amount,
                    publishedAt: new Date()
                }
            }
            
            await strapi.entityService.create("api::transaction.transaction", transaction)

            await strapi.entityService.update("api::account.account", account.id, {
                data: {
                    balance: new_balance
                }
            })

            return transaction
        }
        
    },
    async credit(ctx, next) {

        const requestBody = ctx.request.body.data

        const account = await get_account(requestBody.account_number)
        console.log(account)

        if (account.length == 0) {
            throw new ApplicationError('Invalid account number', { message: `Can not find ${requestBody.account_number} account number` });
        }
        else {
            const new_balance = account.balance + requestBody.amount
            const transaction = {
                data: {
                    journal: await get_journal(),
                    account_number: requestBody.account_number,
                    type: requestBody.type,
                    status: "SUCCEED",
                    action: "CREDIT",
                    amount: requestBody.amount,
                    publishedAt: new Date()
                }
            }

            await strapi.entityService.create("api::transaction.transaction", transaction)

            await strapi.entityService.update("api::account.account", account.id, {
                data: {
                    balance: new_balance
                }
            })

            return transaction

        }
    }
}
```

- Ubah nama file _transaction.js_ menjadi _01-transaction.js_ di folder _src/api/transaction/routes/_

- Create file 02-transactions.js di folder src/api/transaction/routes/, lalu ketikan kode di bawah ini

```javascript
module.exports = {
    routes: [
        { // Path defined with an URL parameter
            method: 'POST',
            path: '/transactions/debit',
            handler: 'custom-transaction.debit',
        },
        { // Path defined with an URL parameter
            method: 'POST',
            path: '/transactions/credit',
            handler: 'custom-transaction.credit',
        },
    ]
}
```
- Re-run kembali aplikasi dengan menjalankan command ini di terminal
```sh
make compose-down; make compose-up
```

- Buka browser, lalu kembali ke http://localhost:8000/admin dan masuk ke menu _Settings > Roles_ lalu pilih _Authenticated_


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/u9mpf4e6lm5qak02z8zo.png)

- Buka _Permissions_ untuk _Account_ dan _Select All_ semua
- Buka _Permissions_ untuk _Customer_ dan _Select All_ semua
- Buka _Permissions_ untuk _Transaction_ dan _Select All_ semua
- Lalu masuk ke menu _Content-Manager > User_ lalu klik _Create new entry_


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dg5o7uyym7uk2ihy4z0s.png)

- Input data sesuai form dan pastikan untuk mengingat _username, email,_ dan _password_. Jangan lupa pilih _confirmed_-nya _true_ dan _role_-nya _Authenticated_


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ukpb5gqikbihapj2qi8c.png)

- Pengembangan aplikasi sudah selesai, teman-teman bisa menjalankan test dengan _Postman_ atau semacamnya. Disini kami menggunakan _Rest Client VSCode extension_ dengan create file _normal.http_ dan menjalankan kode di bawah ini

```http
@host=http://localhost:8000

POST {{host}}/api/auth/local
Content-Type: application/json

{
    "identifier" : "developer",
    "password" : "password"
}

### create customer

POST {{host}}/api/customers
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjgwNDE1MzI1LCJleHAiOjE2ODMwMDczMjV9.HaZpvrouMAjkvm51w0DkRwXjKOdVrInkYvcKfUE_teY

{
    "data" : {
        "id_number" : "3175023005912345",
        "fullname" : "Wildan Anugrah"
    }
}

### create account
POST {{host}}/api/accounts
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjgwNDE1MzI1LCJleHAiOjE2ODMwMDczMjV9.HaZpvrouMAjkvm51w0DkRwXjKOdVrInkYvcKfUE_teY

{
    "data" : {
        "cif_number" : "5685265072",
        "type" : "DEPOSIT"
    }
}

### accounts
GET {{host}}/api/accounts
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjgwNDE1MzI1LCJleHAiOjE2ODMwMDczMjV9.HaZpvrouMAjkvm51w0DkRwXjKOdVrInkYvcKfUE_teY


### credit
POST {{host}}/api/transactions/credit
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjgwNDE1MzI1LCJleHAiOjE2ODMwMDczMjV9.HaZpvrouMAjkvm51w0DkRwXjKOdVrInkYvcKfUE_teY


{
    "data" : {
        "account_number" : "1473367796",
        "payment_type" : "CREDIT",
        "amount" : 10000
    }
}

### debit
POST {{host}}/api/transactions/debit
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNjgwNDE1MzI1LCJleHAiOjE2ODMwMDczMjV9.HaZpvrouMAjkvm51w0DkRwXjKOdVrInkYvcKfUE_teY

{
    "data" : {
        "account_number" : "1473367796",
        "payment_type" : "DEBIT",
        "amount" : 2000
    }
}
```

Cheers :) 

Github: https://github.com/wildananugrah/simple-bank-with-strapi-v4