Files
doom-cc-loan-on-app-docs/README.md
T
Achmad Setyabudi Susilo b4c53c9122 docs(loa): split /submit into /confirm + /submit and add reference_number flow
- Add /confirm endpoint (section 4) — receives the loan data, formats
  display strings, persists context to Redis keyed by reference_number
- /send-otp now requires reference_number; resend reuses the same
  endpoint (no separate /resend-otp)
- /submit-otp requires reference_number and returns a new one for /submit
- /submit now takes only pin + reference_number; loan data is loaded
  from Redis
- Rename term label '3 Bulan' to '3x Cicilan' across all examples
2026-06-29 15:28:32 +07:00

1119 lines
38 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 on 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": {
"eligible": true,
"eligible_note": "Pinjaman baru belum bisa diajukan karena masih ada proses yang berlangsung.",
"admin_fee_note": {
"title": "Ketentuan Biaya Admin",
"description": "1% dari total pinjaman (min. Rp200.000)"
},
"scheme": [
{
"tenor": "3x Cicilan",
"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"
}
],
"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": "3x Cicilan",
"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
}
]
},
{
"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": "3x Cicilan",
"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
}
]
},
{
"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": "3x Cicilan",
"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 `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": "3x Cicilan",
"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/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,
"loan_multiplier": 100000,
"simulation": [
{
"simulation_code": "...",
"term_code": "123",
"monthly_payment": 1700000,
"monthly_payment_string": "Rp1.700.000",
"admin_fee": 200000,
"admin_fee_string": "Rp200.000",
"term": 3,
"term_string": "3x Cicilan",
"interest_rate": 0.5
}
]
}
}
```
</details>
`data` is `usecase.LoaFormResponse` (`internal/interfaces/usecase/loa.go:30`).
> **Frontend validation:** the user-entered `amount` must be a multiple of `loan_multiplier` (e.g. if `100000`, only values like `100000`, `200000`, `1500000`, … are accepted).
> **`simulation` field:** the `/form` response now also includes a pre-fetched list of simulations (same shape as `usecase.LoaTermResponse.simulation`). The simulations are calculated using the card's `maximum_loan` so the frontend can render a sample of available tenors before the user finishes entering an amount. The full per-amount simulation is still returned by `/term`.
### 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) |
---
## 3. `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": "3x Cicilan",
"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`) |
---
## 4. `POST /api/v1/cc-loan-on-app/confirm`
**Erangel route:** `POST /v3-cc-loan-on-app-confirm``erangel/application/controllers/api/v3/loan_on_app/Confirm.php`
**Doom library call:** `Doom::loanconfirm($refnum, $username, $device, $card_token, $amount, $simulation_code, $account_no, $account_name, $term, $interest_rate, $monthly_payment, $admin_fee)``Doom.php:TBD`
**Handler:** `loaHandler.Confirm``loauc.Confirm` (`internal/usecase/loa/confirm.go`)
Confirmation step in the loan flow. Receives the loan details the user just entered on `/form`, formats them for the confirmation screen, persists the loan context to Redis (key = `reference_number`, value = full loan payload), and returns the formatted display strings plus the `reference_number` the frontend must use for the subsequent `/send-otp``/submit-otp``/submit` calls.
The flow is: `/form``/confirm``/send-otp` (call again on the same endpoint to resend) → `/submit-otp``/submit`. Splitting the original single `/submit` call into `/confirm` + `/submit` lets the OTP/PIN verification steps be decoupled from the loan-data submission.
### 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 | from `/form` simulation list |
| `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 |
| `client` | string | [EG] | yes | `BRIMON` |
| `request_refnum` | string | [EG] | yes | len=12 — also used as the `reference_number` returned to the frontend |
| `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
}
```
</details>
<details>
<summary>Erangel → Doom</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.LoaConfirmRequest` (`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": {
"amount": "Rp5.000.000",
"account_no": "123451234512345",
"account_name": "BRItama Bisnis",
"term_string": "3x Cicilan",
"interest_rate": "0,5%",
"admin_fee_string": "Rp50.000",
"monthly_payment_string": "Rp1.700.000",
"reference_number": "123456789154"
}
}
```
</details>
`data` is `usecase.LoaConfirmResponse` (`internal/interfaces/usecase/loa.go:TBD`).
> **`reference_number`:** the same erangel-generated `request_refnum` echoed back from the response `refnum`. Doom stores the full loan payload in Redis under this key. The frontend must send it back to `/send-otp` (and to `/submit` via the new `reference_number` returned by `/submit-otp`).
>
> **Redis payload** (doom-internal, not exposed to FE): `{ card_token, amount, simulation_code, account_no, account_name, term, interest_rate, monthly_payment, admin_fee, otp_type, request_refnum, username, timestamp, channel_id, request_id }`. TTL is set so an abandoned session expires.
### Error Codes
| Code | When |
|---|---|
| `00` | Success — loan context saved to Redis |
| `NA` | `account_no` not in user's financial accounts |
| `TB` | Database/Redis timeout (failed to save loan context) |
| `EH` | Error handling (validation fan-in errors, e.g. invalid `simulation_code` for the chosen `card_token`) |
---
## 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, $reference_number)``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. The `reference_number` links the OTP to a loan context previously stored by `/confirm`.
### Request Body
| Field | Type | Source | Required | Notes |
|---|---|---|---|---|
| `otp_type` | string | [FE] | yes | `WA` or `SMS` — OTP delivery channel |
| `reference_number` | string | [FE] | yes | len=12, `reference_number` returned by `/confirm`; doom uses it to load the loan context from Redis |
| `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",
"reference_number": "123456789154"
}
```
</details>
<details>
<summary>Erangel → Doom</summary>
```json
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"otp_type": "WA",
"reference_number": "123456789154",
"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 |
| `EX` | `reference_number` not found or expired in Redis (no loan context from `/confirm`) |
### 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`).
> **Resend behaviour:** the frontend uses the **same** `/send-otp` endpoint to resend an OTP. Calling it again with the same `reference_number` invalidates the previous `server_id` and returns a fresh one (cipher returns `desc: "Already Exist"` if a previous OTP is still active, and doom forwards that as-is). There is no separate `/resend-otp` endpoint.
---
## 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, $reference_number)` → `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` and must pass the **new** `reference_number` returned here (not the one from `/confirm`/`/send-otp`). 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 the latest `/send-otp` response (whether first call or resend) |
| `otp` | string | [FE] | yes | code entered by user |
| `otp_type` | string | [FE] | yes | `WA` or `SMS` — must match channel used in `/send-otp` |
| `reference_number` | string | [FE] | yes | len=12, `reference_number` returned by `/confirm`; doom uses it to load the loan context from Redis |
| `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",
"reference_number": "123456789154"
}
```
</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",
"reference_number": "123456789154",
"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": {
"reference_number": "987654321098"
}
}
```
</details>
`data.reference_number` is a **new** reference number generated by doom/erangel on successful OTP validation. The frontend must pass it to `/submit` (do **not** reuse the `reference_number` from `/confirm` or `/send-otp`).
### Error Codes
| Code | When |
|---|---|
| `00` | OTP valid |
| `NV` | Invalid OTP |
| `TMT` | Too many wrong-OTP attempts |
| `EX` | OTP not exist or expired, or `reference_number` not found in Redis |
| `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, $reference_number)` → `Doom.php:110`
**Handler:** `loaHandler.Submit` → `loauc.Submit` (`internal/usecase/loa/submit.go`)
Final step: loads the loan context from Redis using `reference_number` (saved by `/confirm`, rotated by `/submit-otp`), submits the loan to Brigate, persists to DB, and returns the new application's progress. The frontend only sends `pin` + `reference_number` — all loan data fields have already been captured by `/confirm` and are not re-validated here.
> **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 |
|---|---|---|---|---|
| `pin` | string | [FE] | yes | consumed by erangel only, not sent to doom |
| `reference_number` | string | [FE] | yes | len=12, `reference_number` returned by `/submit-otp` (NOT the one from `/confirm`); doom uses it to load the loan context from Redis |
| `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
{
"pin": "123456",
"reference_number": "987654321098"
}
```
</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",
"reference_number": "987654321098",
"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": "3x Cicilan",
"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/confirm.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/Confirm.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-on-app/internal/library/cipher/cipher.go`