Files
doom-cc-loan-on-app-docs/README.md
T
Achmad Setyabudi Susilo a0556636fc Update README
2026-06-25 14:30:47 +07:00

1084 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Doom CC Loan In-App API
Base path: `/api/v1/cc-loan-on-app/`
All endpoints accept `POST` and return JSON. Standard response wrapper (defined by `bitbucket.bri.co.id/mod/brimocomp/basic/dto/response/v2`):
```json
{
"code": "00",
"refnum": "<request_refnum>",
"id": "<process_id / request_id>",
"desc": "success",
"data": { ... endpoint-specific payload ... }
}
```
When an error occurs, `data` is wrapped as `{"error": "<error message>"}` (see `internal/utils/utils.go:22`).
## Request Origin: Frontend vs Erangel
Doom is **not** called directly by the frontend. The call flow is:
```
Frontend ──POST──▶ Erangel (API Gateway, CodeIgniter PHP)
└─ Doom library
└─POST──▶ Doom microservice
├─ Vikendi (user/account/safety-mode)
├─ Eredar (credit card / CC list)
├─ Brigate (LOA simulation, submit, status)
├─ Database (static params, LOA persistence)
└─ Cipher-OTP (OTP send / validate) ◀── only send-otp & submit-otp
```
- **Frontend → Erangel**: hits routes like `POST /v3-cc-loan-on-app-onboarding` (mapped in `erangel/application/config/routes.php`). The frontend sends only the user-action fields (see per-endpoint tables).
- **Erangel → Doom**: the `Doom` library (`erangel/application/libraries/microservices/Doom.php`) injects the cross-cutting fields. It also adds HTTP headers: `User-Agent`, `IP-Address`, `Device-Type`, `Device-Id`, `Device-Name`, `Device-Version`.
- **Doom → Cipher-OTP**: triggered only by the `send-otp` and `submit-otp` endpoints (see the "Downstream: Cipher-OTP" subsections).
Field source legend used below:
- **[FE]** = set by the **frontend** in the body sent to erangel
- **[EG]** = **erangel**-generated/injected when forwarding to doom (not in the frontend body)
- **[EG→DM]** = arrives at doom via erangel-injected field (originated from session/config/timestamp)
- **[FE→EG→DM]** = flows frontend → erangel → doom unchanged
- **[DM]** = **doom** server-side (overwritten inside the handler, or hardcoded)
### Common Request Fields (all endpoints)
| Field | Type | Required | Source | Validation | Notes |
|---|---|---|---|---|---|
| `client` | string | yes | [EG] | `oneof=BRIMON` | From erangel config `doom_client` |
| `request_refnum` | string | yes | [EG] | `len=12` | Generated by erangel (`get_reference_number()`); echoed in `refnum` |
| `username` | string | yes | [EG] | `len=10` | From erangel auth session (`get_session_username()`) |
| `timestamp` | string | yes | [EG] | `len=13` | Generated by erangel via `round(microtime(TRUE) * 1000)` |
| `channel_id` | string | yes | [EG] | `oneof=NBMB` | From erangel config `doom_channel_id` |
| `request_id` | string | no | [DM] | | Not sent by erangel; overwritten in handler with the process ID |
### Common Response Codes
| Code | Constant | Description |
|---|---|---|
| `00` | `RCSuccess` | Success |
| `01` | `RC01` | No credit card / empty list |
| `VE` | `RCValidationError` | Invalid request body |
| `EH` | `RCErrorHandling` | Error handling |
| `NF` | `RCNotFound` | Not found |
| `NA` | `RCNotAuthorized` | Not authorized |
| `CNF` | `RCCreditCardNotFound` | Credit card not found |
| `SM` | `RCUserSafetyMode` | Safety mode active |
| `FM` | `RCFM` | Malformed JSON |
| `TV` | `RCTimeoutVikendi` | Vikendi timeout |
| `TB` | `RCTimeoutDB` | Database timeout |
| `TBR` | `RCTimeoutBrigate` | Brigate timeout |
| `TC` | `RCTimeoutMicroOTP` | Cipher/OTP timeout |
| `TE` | `RCTimeoutEredar` | Eredar timeout |
| `SK` | `RCSkipOtp` (cipher) | OTP skipped (disabled in config) — passed through from `send-otp` |
| `NV` | `RCOtpNotValid` (cipher) | Invalid OTP — passed through from `submit-otp` |
| `TMT` | `RCOtpTooManyTries` (cipher) | Too many wrong-OTP attempts |
| `EX` | `RCOtpNotExist` (cipher) | OTP not exist or expired |
| `TR` | `RCTimeoutRedis` (cipher) | Redis timeout (OTP store) |
| `TK` | `RCTimeoutKafka` (cipher) | Kafka timeout (OTP SMS/WA delivery) |
| `THV` | `RCTimeoutHaven` (cipher) | Haven timeout |
Messages are localized in `id` or `en` (selected via the `language` header / middleware).
---
## 1. `POST /api/v1/cc-loan-on-app/onboarding`
**Erangel route:** `POST /v3-cc-loan-on-app-onboarding``erangel/application/controllers/api/v3/loan_on_app/Onboarding.php`
**Doom library call:** `Doom::onboarding($refnum, $username, $device)``Doom.php:68`
**Handler:** `loaHandler.Onboarding``loauc.Onboarding` (`internal/usecase/loa/onboarding.go`)
Loads the onboarding screen: scheme/slider text configuration and the user's existing loan applications grouped by status.
### Request Body
| Field | Source | Required |
|---|---|---|
| _no body fields_ | | |
The frontend sends **no body fields** to erangel for this endpoint. All common fields below are injected by erangel.
<details>
<summary>Request Body JSON</summary>
```json
{
"client": "BRIMON", // [EG]
"request_refnum": "123456789154", // [EG]
"username": "acctest009", // [EG]
"timestamp": "1652034352767", // [EG]
"channel_id": "NBMB", // [EG]
"request_id": "f6eb58d24d6842f59ddb40f40d2a7992" // [DM] (overwritten)
}
```
</details>
Struct: `usecase.OnboardingRequest` (`internal/interfaces/usecase/loa.go:51`)
### Response Body (200)
<details>
<summary>Response Body JSON</summary>
```json
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"admin_fee_note": {
"title": "Ketentuan Biaya Admin",
"description": "1% dari total pinjaman (min. Rp200.000)"
},
"scheme": [
{
"tenor": "3 Bulan",
"bunga": "0%"
},
{
"tenor": "6 Bulan",
"bunga": "0,5%"
},
{
"tenor": "12 Bulan",
"bunga": "0,5%"
},
{
"tenor": "15 Bulan",
"bunga": "0,99%"
},
{
"tenor": "18 Bulan",
"bunga": "0,99%"
},
{
"tenor": "24 Bulan",
"bunga": "0,99%"
},
{
"tenor": "36 Bulan",
"bunga": "0,99%"
}
],
"slider_texts": [
{
"title": "Pinjaman Fleksibel",
"description": "Pinjaman dana minimal 1 juta hingga 75 juta atau 50% dari sisa limit kartu kredit.",
"image_path": "http://asset-host/example.png"
},
{
"title": "Pinjaman Fleksibel",
"description": "Pinjaman dana minimal 1 juta hingga 75 juta atau 50% dari sisa limit kartu kredit.",
"image_path": "http://asset-host/example.png"
},
{
"title": "Pinjaman Fleksibel",
"description": "Pinjaman dana minimal 1 juta hingga 75 juta atau 50% dari sisa limit kartu kredit.",
"image_path": "http://asset-host/example.png"
}
],
"processing": {
"count": 1,
"list": [
{
"ticket_no": "TKT001",
"card_no": "2003 **** **** 4302",
"card_name": "Agung Harsono",
"card_product_type": "Easy Card",
"card_image_name": "bri_easy_card",
"card_image_path": "http://asset-host/card/bri_easy_card.png",
"account_no": "029012345678901",
"account_string": "0290 **** **** 112",
"account_name": "BRItama Bisnis",
"account_image_name": "britama_bisnis",
"account_image_path": "http://asset-host/account/britama_bisnis.png",
"status": "PROCESSING",
"status_string": "Pengajuan Diproses",
"request_time": "09 February 2026, 09:41:02 WIB",
"amount": "Rp3.200.000",
"monthly_payment": "Rp1.015.000",
"admin_fee": "Rp200.000",
"interest_rate": "0%",
"term": "3 Bulan",
"seq_no": 1,
"progress": [
{
"status_title": "Pengajuan dalam Analisis",
"status_time": "09 Feb 2026, 09:41 WIB",
"order": 1,
"current": true
},
{
"status_title": "Pengajuan Disetujui",
"status_time": "",
"order": 2,
"current": false
}
]
}
]
},
"rejected": {
"count": 1,
"list": [
{
"ticket_no": "TKT002",
"card_no": "2003 **** **** 4302",
"card_name": "Agung Harsono",
"card_product_type": "Easy Card",
"card_image_name": "bri_easy_card",
"card_image_path": "http://asset-host/card/bri_easy_card.png",
"account_no": "029012345678901",
"account_string": "0290 **** **** 112",
"account_name": "BRItama Bisnis",
"account_image_name": "britama_bisnis",
"account_image_path": "http://asset-host/account/britama_bisnis.png",
"status": "REJECTED",
"status_string": "Pengajuan Ditolak",
"request_time": "09 February 2026, 09:41:02 WIB",
"amount": "Rp3.200.000",
"monthly_payment": "Rp1.015.000",
"admin_fee": "Rp200.000",
"interest_rate": "0%",
"term": "3 Bulan",
"seq_no": 2,
"progress": [
{
"status_title": "Pengajuan dalam Analisis",
"status_time": "09 Feb 2026, 09:41 WIB",
"order": 1,
"current": false
},
{
"status_title": "Pengajuan Ditolak",
"status_time": "10 Feb 2026, 06:30 WIB",
"order": 2,
"current": true
}
]
}
]
},
"approved": {
"count": 1,
"list": [
{
"ticket_no": "TKT003",
"card_no": "2003 **** **** 4302",
"card_name": "Agung Harsono",
"card_product_type": "Easy Card",
"card_image_name": "bri_easy_card",
"card_image_path": "http://asset-host/card/bri_easy_card.png",
"account_no": "029012345678901",
"account_string": "0290 **** **** 112",
"account_name": "BRItama Bisnis",
"account_image_name": "britama_bisnis",
"account_image_path": "http://asset-host/account/britama_bisnis.png",
"status": "APPROVED",
"status_string": "Pengajuan Disetujui",
"request_time": "09 February 2026, 09:41:02 WIB",
"amount": "Rp3.200.000",
"monthly_payment": "Rp1.015.000",
"admin_fee": "Rp200.000",
"interest_rate": "0%",
"term": "3 Bulan",
"seq_no": 3,
"progress": [
{
"status_title": "Pengajuan dalam Analisis",
"status_time": "09 Feb 2026, 09:41 WIB",
"order": 1,
"current": false
},
{
"status_title": "Pengajuan Disetujui",
"status_time": "10 Feb 2026, 06:30 WIB",
"order": 2,
"current": true
}
]
}
]
}
}
}
```
</details>
`data` is `usecase.OnboardingResponse`.
`admin_fee_note` carries the admin fee info banner shown in the scheme bottom sheet (title: "Ketentuan Biaya Admin", content: "1% dari total pinjaman (min. Rp200.000)").
Each `scheme` item now only contains `tenor` and `bunga` (removed `nominal_transaksi` and `subtitle_transaksi`).
Each item in `processing`/`rejected`/`approved.list` is `usecase.Application`:
<details>
<summary>Application item schema</summary>
```json
{
"ticket_no": "...",
"card_no": "2003 **** **** 4302",
"card_name": "Agung Harsono",
"card_product_type": "Easy Card",
"card_image_name": "bri_easy_card",
"card_image_path": "http://asset-host/card/bri_easy_card.png",
"account_no": "029012345678901",
"account_string": "0290 **** **** 112",
"account_name": "BRItama Bisnis",
"account_image_name": "britama_bisnis",
"account_image_path": "http://asset-host/account/britama_bisnis.png",
"status": "PROCESSING|APPROVED|REJECTED",
"status_string": "Pengajuan Diproses|Pengajuan Disetujui|Pengajuan Ditolak",
"request_time": "09 February 2026, 09:41:02 WIB",
"amount": "Rp3.200.000",
"monthly_payment": "Rp1.015.000",
"admin_fee": "Rp200.000",
"interest_rate": "0%",
"term": "3 Bulan",
"seq_no": 1,
"progress": [
{
"status_title": "Pengajuan dalam Analisis",
"status_time": "09 Feb 2026, 09:41 WIB",
"order": 1,
"current": true
},
{
"status_title": "Pengajuan Disetujui|Pengajuan Ditolak",
"status_time": "",
"order": 2,
"current": false
}
]
}
```
</details>
**`status` / `status_string` mapping:**
| `status` | `status_string` | Description |
|---|---|---|
| `PROCESSING` | `Pengajuan Diproses` | Loan submitted, under review |
| `APPROVED` | `Pengajuan Disetujui` | Loan approved |
| `REJECTED` | `Pengajuan Ditolak` | Loan rejected |
### Error Codes
| Code | When |
|---|---|
| `00` | `success` also returned when user has no CC (CC list empty from Eredar). |
| `TV` | Vikendi timeout (Eredar call) |
| `NA` | Not authorized (Eredar call) |
| `TB` | Static param DB read failure |
| `TBR` | Brigate timeout on `GetLoaStatusFromBrigate` |
---
## 2. `POST /api/v1/cc-loan-on-app/list`
**Erangel route:** `POST /v3-cc-loan-on-app-card-list``erangel/application/controllers/api/v3/loan_on_app/List_loa.php`
**Doom library call:** `Doom::list($refnum, $username, $device)``Doom.php:148`
**Handler:** `loaHandler.List``loauc.List` (`internal/usecase/loa/list.go`)
Lists all the credit cards the user owns that are eligible for loan-on-app.
### Request Body
| Field | Source | Required |
|---|---|---|
| _no body fields_ | | |
The frontend sends **no body fields** to erangel for this endpoint. All common fields below are injected by erangel.
<details>
<summary>Request Body JSON</summary>
```json
{
"client": "BRIMON", // [EG]
"request_refnum": "123456789154", // [EG]
"username": "acctest009", // [EG]
"timestamp": "1652034352767", // [EG]
"channel_id": "NBMB", // [EG]
"request_id": "f6eb58d24d6842f59ddb40f40d2a7992" // [DM] (overwritten)
}
```
</details>
Struct: `usecase.LoaListRequest` (`internal/interfaces/usecase/loa.go:186`)
### Response Body (200)
<details>
<summary>Response Body JSON</summary>
```json
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"account": [
{
"card_number_token": "f104d035a92652987b8c82dd91cb13aa...",
"card_number_string": "1234 **** **** 5678",
"name": "JOHN DOE",
"currency": "IDR",
"image_name": "jcb_platinum_crop",
"image_path": "http://asset-host/card/jcb_platinum_crop.png",
"card_block": "00",
"detail_type": "cc",
"cif": "...",
"financial_status": 1,
"is_kkp": false
}
]
}
}
```
</details>
`data` is `usecase.LoaListResponse`. When the user has no eligible cards, `code` is `01` and `account` is an empty array (see `list.go:83-86`).
### Error Codes
| Code | When |
|---|---|
| `00` | Success (cards found) |
| `01` | Empty list / no credit card found |
| `NA`, `TV` | Eredar call failed |
| `TB` | Vikendi / DB read failure |
---
## 3. `POST /api/v1/cc-loan-on-app/form`
**Erangel route:** `POST /v3-cc-loan-on-app-form``erangel/application/controllers/api/v3/loan_on_app/Form.php`
**Doom library call:** `Doom::loanform($refnum, $username, $device, $card_token)``Doom.php:81`
**Handler:** `loaHandler.Form``loauc.Form` (`internal/usecase/loa/form.go`)
Loads the loan form: source accounts, T&C text, product type, min/max loan range.
### Request Body
| Field | Type | Source | Required | Notes |
|---|---|---|---|---|
| `card_token` | string | [FE] | yes | len=64, validated by doom |
| `client` | string | [EG] | yes | `BRIMON` |
| `request_refnum` | string | [EG] | yes | len=12 |
| `username` | string | [EG] | yes | len=10, from session |
| `timestamp` | string | [EG] | yes | len=13, server-generated |
| `channel_id` | string | [EG] | yes | `NBMB` |
| `request_id` | string | [DM] | no | overwritten by doom with process ID |
<details>
<summary>Frontend → Erangel</summary>
```json
{
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0"
}
```
</details>
<details>
<summary>Erangel → Doom (constructed in Doom.php:81)</summary>
```json
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"request_id": "<overwritten by doom>"
}
```
</details>
Struct: `usecase.LoaFormRequest` (`internal/interfaces/usecase/loa.go:20`)
### Response Body (200)
<details>
<summary>Response Body JSON</summary>
```json
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"account_list": [
{
"account": "123451234512345",
"account_string": "1234 5123 4512 345",
"name": "supedi",
"currency": "IDR",
"alias": "Alias-supedi",
"product_type": "karti",
"image_name": "karti.png",
"image_path": "http://.../karti.png",
"default": 1
}
],
"balance_string": "Rp20.000.000,00",
"loan_note":"Pinjaman mulai Rp1 juta s.d Rp75 juta, maksimal 50% dari sisa limit.",
"tnc_text": "tnc-id",
"product_type": "Jcb Platinum",
"cc_image_slim": "http://.../card/jcb_platinum_slim.png",
"minimum_loan": 1000000,
"maximum_loan": 20000000
}
}
```
</details>
`data` is `usecase.LoaFormResponse` (`internal/interfaces/usecase/loa.go:30`).
### Error Codes
| Code | When |
|---|---|
| `00` | Success |
| `SM` | User is in safety mode (`form.go:37`) |
| `CNF` | Credit card not found (`form.go:100`) |
| `TV` | Vikendi timeout (Eredar, parameter, account list) |
| `TB` | Database timeout (static param) |
| `TBR` | Brigate timeout (`GetProgramTermFromBrigate`) |
| `EH` | Error handling (account alias/validation fan-in errors) |
---
## 4. `POST /api/v1/cc-loan-on-app/term`
**Erangel route:** `POST /v3-cc-loan-on-app-term``erangel/application/controllers/api/v3/loan_on_app/Term.php`
**Doom library call:** `Doom::loanterm($refnum, $username, $device, $card_token, $amount)``Doom.php:95`
**Handler:** `loaHandler.Term``loauc.Term` (`internal/usecase/loa/term.go`)
Returns a list of loan simulations (tenor / monthly payment / admin fee) for a given amount.
### Request Body
| Field | Type | Source | Required | Notes |
|---|---|---|---|---|
| `card_token` | string | [FE] | yes | |
| `amount` | int | [FE] | yes | doom validates `gt=0` |
| `client` | string | [EG] | yes | `BRIMON` |
| `request_refnum` | string | [EG] | yes | len=12 |
| `username` | string | [EG] | yes | len=10, from session |
| `timestamp` | string | [EG] | yes | len=13, server-generated |
| `channel_id` | string | [EG] | yes | `NBMB` |
| `request_id` | string | [DM] | no | overwritten by doom with process ID |
<details>
<summary>Frontend → Erangel</summary>
```json
{
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"amount": 5000
}
```
</details>
<details>
<summary>Erangel → Doom (constructed in Doom.php:95)</summary>
```json
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"amount": 5000,
"request_id": "<overwritten by doom>"
}
```
</details>
Struct: `usecase.LoaTermRequest` (`internal/interfaces/usecase/loa.go:104`)
### Response Body (200)
<details>
<summary>Response Body JSON</summary>
```json
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"simulation": [
{
"simulation_code": "...",
"term_code": "123",
"monthly_payment": 1015000,
"monthly_payment_string": "Rp1.015.000",
"admin_fee": 5000,
"admin_fee_string": "Rp5.000",
"term": 3,
"term_string": "3 Bulan",
"interest_rate": 0.5
}
]
}
}
```
</details>
`data` is `usecase.LoaTermResponse` (`internal/interfaces/usecase/loa.go:115`).
### Error Codes
| Code | When |
|---|---|
| `00` | Success |
| `TBR` | Brigate timeout (`GetLoaSimulationtFromBrigate`) |
---
## 5. `POST /api/v1/cc-loan-on-app/send-otp`
**Erangel route:** `POST /v3-cc-loan-on-app-send-otp``erangel/application/controllers/api/v3/loan_on_app/Send_otp.php`
**Doom library call:** `Doom::loansendotp($refnum, $username, $device, $otp_type)``Doom.php:134`
**Handler:** `loaHandler.SendOtp``loauc.SendOtp` (`internal/usecase/loa/send_otp.go`)
Sends a one-time password to the user's registered phone number for the loan-on-app flow.
### Request Body
| Field | Type | Source | Required | Notes |
|---|---|---|---|---|
| `otp_type` | string | [FE] | yes | `WA` or `SMS` — OTP delivery channel |
| `client` | string | [EG] | yes | `BRIMON` |
| `request_refnum` | string | [EG] | yes | len=12 |
| `username` | string | [EG] | yes | len=10, from session |
| `timestamp` | string | [EG] | yes | len=13, server-generated |
| `channel_id` | string | [EG] | yes | `NBMB` |
| `request_id` | string | [DM] | no | overwritten by doom with process ID |
<details>
<summary>Frontend → Erangel</summary>
```json
{
"otp_type": "WA"
}
```
</details>
<details>
<summary>Erangel → Doom</summary>
```json
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"otp_type": "WA",
"request_id": "<overwritten by doom>"
}
```
</details>
Struct: `usecase.LoaSendOtpRequest` (`internal/interfaces/usecase/loa.go:177`)
### Response Body (200)
<details>
<summary>Response Body JSON</summary>
```json
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"server_id": "abc123",
"cellphone_number": "0812****1234",
"duration_sec": 300
}
}
```
</details>
If `otp_type=loanInApp` is disabled in cipher config, doom returns `code: "00"` with `data: { "skip": true }` — frontend skips OTP entry and proceeds to `/submit-otp` without an OTP code.
### Error Codes
| Code | When |
|---|---|
| `00` | Success |
| `TV` | Vikendi timeout (fetching user phone number) |
| `TC` | Cipher/OTP timeout |
| `TR` | Redis timeout (cipher) |
| `TK` | Kafka timeout — OTP SMS/WA delivery failed |
### Downstream: Cipher-OTP `POST /api/v1/otp/send`
Doom fetches the user's phone number from **Vikendi** (via `username`), then proxies the send request to **cipher-otp**.
**Request forwarded to cipher-otp:**
| Field | Type | Source | Notes |
|---|---|---|---|
| `client` | string | [EG→DM] | `BRIMON` |
| `request_refnum` | string | [EG→DM] | len=12 |
| `username` | string | [EG→DM] | len=10 |
| `timestamp` | string | [EG→DM] | len=13 |
| `channel_id` | string | [EG→DM] | `NBMB` |
| `otp_type` | string | [DM] | hardcoded `loanInApp` (`constant.OtpTypeLoanInApp`) |
| `phone_no` | string | [DM] | from user profile returned by Vikendi |
| `method` | string | [FE→EG→DM] | from `otp_type` field (`WA` or `SMS`) |
`request_id` is **not** forwarded — cipher-otp generates its own.
- If an OTP is already active in Redis, cipher returns `desc: "Already Exist"` with the same `server_id`/`duration_sec` (`send.go:84-95`).
---
## 6. `POST /api/v1/cc-loan-on-app/submit-otp`
**Erangel route:** `POST /v3-cc-loan-on-app-submit-otp``erangel/application/controllers/api/v3/loan_on_app/Submit_otp.php`
**Doom library call:** `Doom::loansubmitotp($refnum, $username, $device, $server_id, $otp, $otp_type)``Doom.php:TBD`
**Handler:** `loaHandler.SubmitOtp``loauc.SubmitOtp` (`internal/usecase/loa/submit_otp.go`)
Validates the OTP the user received via `/send-otp`. On success the frontend proceeds to `/submit`. Modelled after `haven-credit-card /api/v1/apply-cc/submit-otp` (see `erangel/application/libraries/microservices/Haven.php:submit_otp` and `erangel/application/controllers/api/v3/credit_card_application/Submit_otp.php`).
### Request Body
| Field | Type | Source | Required | Notes |
|---|---|---|---|---|
| `server_id` | string | [FE] | yes | from `/send-otp` response |
| `otp` | string | [FE] | yes | code entered by user |
| `otp_type` | string | [FE] | yes | `WA` or `SMS` — must match channel used in `/send-otp` |
| `client` | string | [EG] | yes | `BRIMON` |
| `request_refnum` | string | [EG] | yes | len=12 |
| `username` | string | [EG] | yes | len=10, from session |
| `timestamp` | string | [EG] | yes | len=13, server-generated |
| `channel_id` | string | [EG] | yes | `NBMB` |
| `request_id` | string | [DM] | no | overwritten by doom with process ID |
<details>
<summary>Frontend → Erangel</summary>
```json
{
"server_id": "abc123",
"otp": "123456",
"otp_type": "WA"
}
```
</details>
<details>
<summary>Erangel → Doom</summary>
```json
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"server_id": "abc123",
"otp": "123456",
"otp_type": "WA",
"request_id": "<overwritten by doom>"
}
```
</details>
Struct: `usecase.LoaSubmitOtpRequest` (`internal/interfaces/usecase/loa.go:TBD`)
### Response Body (200)
<details>
<summary>Response Body JSON</summary>
```json
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": null
}
```
</details>
No data payload on success — frontend simply checks `code == "00"` before proceeding to `/submit`.
### Error Codes
| Code | When |
|---|---|
| `00` | OTP valid |
| `NV` | Invalid OTP |
| `TMT` | Too many wrong-OTP attempts |
| `EX` | OTP not exist or expired |
| `TC` | Cipher/OTP validation timeout |
| `TR` | Redis timeout (cipher) |
### Downstream: Cipher-OTP `POST /api/v1/otp/validate`
Doom forwards the validate call to cipher-otp:
| Field | Type | Source | Notes |
|---|---|---|---|
| `client` | string | [EG→DM] | `BRIMON` |
| `request_refnum` | string | [EG→DM] | len=12 |
| `username` | string | [EG→DM] | len=10 |
| `timestamp` | string | [EG→DM] | len=13 |
| `channel_id` | string | [EG→DM] | `NBMB` |
| `otp_type` | string | [DM] | hardcoded `loanInApp` (`constant.OtpTypeLoanInApp`) — doom maps FE's `WA`/`SMS` to this internally |
| `server_id` | string | [FE→EG→DM] | from request |
| `otp` | string | [FE→EG→DM] | from request |
Cipher validate logic:
1. Rate-limit check in Redis → `TMT` if exceeded.
2. If `otp_type=loanInApp` disabled in cipher config → returns `00` (skip).
3. Reads OTP from Redis by `(otp_type, username)``EX` if missing.
4. Compares `server_id` + `otp``NV` on mismatch (increments rate-limit counter).
5. On match: deletes OTP from Redis, writes `otp_history`, returns `00`.
A failed validate returns immediately — cipher's `code`/`desc` forwarded 1:1 to the frontend.
---
## 7. `POST /api/v1/cc-loan-on-app/submit`
**Erangel route:** `POST /v3-cc-loan-on-app-submit``erangel/application/controllers/api/v3/loan_on_app/Submit.php`
**Doom library call:** `Doom::loansubmit($refnum, $username, $device, $card_token, $amount, $simulation_code, $account_no, $account_name, $term, $interest_rate, $monthly_payment, $admin_fee, $pin)``Doom.php:110`
**Handler:** `loaHandler.Submit``loauc.Submit` (`internal/usecase/loa/submit.go`)
Final step: submits the loan to Brigate, persists to DB, and returns the new application's progress. OTP validation has moved to `/submit-otp`; this endpoint now requires PIN instead.
> **Erangel PIN validation (`Submit.php`):** the frontend must send `pin`. Erangel validates it via `check_pin()` **before** calling doom. The `pin` value is **not** forwarded to doom.
### Request Body
| Field | Type | Source | Required | Notes |
|---|---|---|---|---|
| `card_token` | string | [FE] | yes | |
| `amount` | number | [FE] | yes | doom: `gt=0`; erangel casts to int before forwarding |
| `simulation_code` | string | [FE] | yes | |
| `account_no` | string | [FE] | yes | |
| `account_name` | string | [FE] | yes | |
| `term` | int | [FE] | yes | erangel casts to int before forwarding |
| `interest_rate` | number | [FE] | yes | erangel casts to float |
| `monthly_payment` | number | [FE] | yes | doom: `gt=0`; erangel casts to float |
| `admin_fee` | number | [FE] | yes | doom: `gte=0`; erangel casts to float |
| `pin` | string | [FE] | yes | consumed by erangel only, not sent to doom |
| `client` | string | [EG] | yes | `BRIMON` |
| `request_refnum` | string | [EG] | yes | len=12 |
| `username` | string | [EG] | yes | len=10, from session |
| `timestamp` | string | [EG] | yes | len=13, server-generated |
| `channel_id` | string | [EG] | yes | `NBMB` |
| `request_id` | string | [DM] | no | overwritten by doom with process ID |
<details>
<summary>Frontend → Erangel</summary>
```json
{
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"amount": 5000000,
"simulation_code": "...",
"account_no": "123451234512345",
"account_name": "BRItama Bisnis",
"term": 3,
"interest_rate": 0.5,
"monthly_payment": 1700000,
"admin_fee": 50000,
"pin": "123456"
}
```
</details>
<details>
<summary>Erangel → Doom (pin consumed by erangel, not forwarded)</summary>
```json
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"amount": 5000000,
"simulation_code": "...",
"account_no": "123451234512345",
"account_name": "BRItama Bisnis",
"term": 3,
"interest_rate": 0.5,
"monthly_payment": 1700000.0,
"admin_fee": 50000.0,
"request_id": "<overwritten by doom>"
}
```
</details>
Struct: `usecase.LoaSubmitRequest` (`internal/interfaces/usecase/loa.go:131`)
### Response Body (200)
<details>
<summary>Response Body JSON</summary>
```json
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"ticket_no": "TKT001",
"card_no": "2003 **** **** 4302",
"card_name": "Agung Harsono",
"card_product_type": "Easy Card",
"card_image_name": "bri_easy_card",
"card_image_path": "http://asset-host/card/bri_easy_card.png",
"account_no": "029012345678901",
"account_string": "0290 **** **** 112",
"account_name": "BRItama Bisnis",
"account_image_name": "britama_bisnis",
"account_image_path": "http://asset-host/account/britama_bisnis.png",
"request_time": "09 February 2026, 09:41:02 WIB",
"amount": "Rp3.200.000",
"monthly_payment": "Rp1.015.000",
"admin_fee": "Rp200.000",
"interest_rate": "0%",
"term": "3 Bulan",
"sla_note": "Proses pengajuan 1x24 jam kerja. Kamu akan diberi notifikasi saat pencairan disetujui.",
"status": "PROCESSING|APPROVED|REJECTED",
"status_string": "Pengajuan Diproses|Pengajuan Disetujui|Pengajuan Ditolak",
"progress": [
{
"status_title": "Pengajuan dalam Analisis",
"status_time": "09 Feb 2026, 09:41 WIB",
"order": 1,
"current": true
},
{
"status_title": "Pengajuan Disetujui|Pengajuan Ditolak",
"status_time": "",
"order": 2,
"current": false
}
]
}
}
```
</details>
`data` is `usecase.LoaSubmitResponse` (`internal/interfaces/usecase/loa.go:151`).
### Error Codes
| Code | When |
|---|---|
| `00` | Success |
| `NA` | `account_no` not in user's financial accounts (`submit.go:44`) |
| `TBR` | Brigate `LoaSubmitToBrigate` failed |
| `TB` | DB insert failed (`InsertLoanOnAppToDB`) or `GetLoaStatusFromBrigate` DB error |
---
## 8. `GET /healthz`
Health check. Defined in `internal/delivery/http/http.go:47`. Returns literal `"ok"`.
<details>
<summary>Response body</summary>
```json
"ok"
```
</details>
---
## Source Code References
### Doom (this microservice)
- Routes: `internal/delivery/http/http.go:39-50`
- Handlers: `internal/delivery/http/loa.go`
- Request/Response structs: `internal/interfaces/usecase/loa.go`
- Usecase logic:
- `internal/usecase/loa/onboarding.go`
- `internal/usecase/loa/list.go`
- `internal/usecase/loa/form.go`
- `internal/usecase/loa/term.go`
- `internal/usecase/loa/send_otp.go`
- `internal/usecase/loa/submit_otp.go`
- `internal/usecase/loa/submit.go`
- Response codes & messages: `constant/response_code.go`
- Constants & route prefixes: `constant/app.go`
### Erangel (API Gateway, sibling repo)
- Routes mapping (frontend-facing URLs): `erangel/application/config/routes.php`
- Doom client library: `erangel/application/libraries/microservices/Doom.php`
- Frontend-facing controllers (each validates FE body, then calls `Doom::*`):
- `erangel/application/controllers/api/v3/loan_on_app/Onboarding.php`
- `erangel/application/controllers/api/v3/loan_on_app/List_loa.php`
- `erangel/application/controllers/api/v3/loan_on_app/Form.php`
- `erangel/application/controllers/api/v3/loan_on_app/Term.php`
- `erangel/application/controllers/api/v3/loan_on_app/Send_otp.php`
- `erangel/application/controllers/api/v3/loan_on_app/Submit_otp.php`
- `erangel/application/controllers/api/v3/loan_on_app/Submit.php`
- Base controller (auth/session, PIN checks, device, refnum generation): `erangel/application/libraries/api/Transaction_Controller.php`
### Cipher-OTP (downstream microservice called by `send-otp` and `submit-otp`)
Base path: `/api/v1/otp/` (v1) and `/api/v5/otp/` (v5). Doom uses the v1 endpoints.
- Routes: `cipher-otp/internal/delivery/http/http.go:40-44`
- Handlers: `cipher-otp/internal/delivery/http/otp.go`
- Request/Response structs: `cipher-otp/internal/interfaces/usecase/otp.go`
- Usecase logic:
- `cipher-otp/internal/usecase/otp/send.go` (handles skip-OTP, already-active-OTP, Kafka notification)
- `cipher-otp/internal/usecase/otp/validate.go` (handles rate limit, server_id+otp match, history insert)
- Response wrapper: `cipher-otp/internal/pkg/dto/basic.go` (uses `response_code`/`response_refnum`/`response_id`/`response_desc`/`response_data`)
- Response codes & messages: `cipher-otp/constant/response_code.go` (notable additions: `SK`, `NV`, `TMT`, `EX`, `TR`, `TK`, `THV`)
- Doom's cipher client: `doom-cc-loan-in-app/internal/library/cipher/cipher.go`