- 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
38 KiB
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):
{
"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 inerangel/application/config/routes.php). The frontend sends only the user-action fields (see per-endpoint tables). - Erangel → Doom: the
Doomlibrary (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-otpandsubmit-otpendpoints (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.
Request Body JSON
{
"client": "BRIMON", // [EG]
"request_refnum": "123456789154", // [EG]
"username": "acctest009", // [EG]
"timestamp": "1652034352767", // [EG]
"channel_id": "NBMB", // [EG]
"request_id": "f6eb58d24d6842f59ddb40f40d2a7992" // [DM] (overwritten)
}
Struct: usecase.OnboardingRequest (internal/interfaces/usecase/loa.go:51)
Response Body (200)
Response Body 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
}
]
}
]
}
}
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:
Application item schema
{
"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
}
]
}
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 |
Frontend → Erangel
{
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0"
}
Erangel → Doom (constructed in Doom.php:81)
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"request_id": "<overwritten by doom>"
}
Struct: usecase.LoaFormRequest (internal/interfaces/usecase/loa.go:20)
Response Body (200)
Response Body 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
}
]
}
}
data is usecase.LoaFormResponse (internal/interfaces/usecase/loa.go:30).
Frontend validation: the user-entered
amountmust be a multiple ofloan_multiplier(e.g. if100000, only values like100000,200000,1500000, … are accepted).
simulationfield: the/formresponse now also includes a pre-fetched list of simulations (same shape asusecase.LoaTermResponse.simulation). The simulations are calculated using the card'smaximum_loanso 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 |
Frontend → Erangel
{
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"amount": 5000
}
Erangel → Doom (constructed in Doom.php:95)
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0",
"amount": 5000,
"request_id": "<overwritten by doom>"
}
Struct: usecase.LoaTermRequest (internal/interfaces/usecase/loa.go:104)
Response Body (200)
Response Body 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
}
]
}
}
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 |
Frontend → Erangel
{
"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
}
Erangel → Doom
{
"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>"
}
Struct: usecase.LoaConfirmRequest (internal/interfaces/usecase/loa.go:TBD)
Response Body (200)
Response Body 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"
}
}
data is usecase.LoaConfirmResponse (internal/interfaces/usecase/loa.go:TBD).
reference_number: the same erangel-generatedrequest_refnumechoed back from the responserefnum. Doom stores the full loan payload in Redis under this key. The frontend must send it back to/send-otp(and to/submitvia the newreference_numberreturned 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 |
Frontend → Erangel
{
"otp_type": "WA",
"reference_number": "123456789154"
}
Erangel → Doom
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"otp_type": "WA",
"reference_number": "123456789154",
"request_id": "<overwritten by doom>"
}
Struct: usecase.LoaSendOtpRequest (internal/interfaces/usecase/loa.go:177)
Response Body (200)
Response Body JSON
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"server_id": "abc123",
"cellphone_number": "0812****1234",
"duration_sec": 300
}
}
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 sameserver_id/duration_sec(send.go:84-95).
Resend behaviour: the frontend uses the same
/send-otpendpoint to resend an OTP. Calling it again with the samereference_numberinvalidates the previousserver_idand returns a fresh one (cipher returnsdesc: "Already Exist"if a previous OTP is still active, and doom forwards that as-is). There is no separate/resend-otpendpoint.
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 |
Frontend → Erangel
{
"server_id": "abc123",
"otp": "123456",
"otp_type": "WA",
"reference_number": "123456789154"
}
Erangel → Doom
{
"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>"
}
Struct: usecase.LoaSubmitOtpRequest (internal/interfaces/usecase/loa.go:TBD)
Response Body (200)
Response Body JSON
{
"code": "00",
"refnum": "123456789154",
"id": "<process_id>",
"desc": "success",
"data": {
"reference_number": "987654321098"
}
}
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:
- Rate-limit check in Redis →
TMTif exceeded. - If
otp_type=loanInAppdisabled in cipher config → returns00(skip). - Reads OTP from Redis by
(otp_type, username)→EXif missing. - Compares
server_id+otp→NVon mismatch (increments rate-limit counter). - On match: deletes OTP from Redis, writes
otp_history, returns00.
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 sendpin. Erangel validates it viacheck_pin()before calling doom. Thepinvalue 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 |
Frontend → Erangel
{
"pin": "123456",
"reference_number": "987654321098"
}
Erangel → Doom (pin consumed by erangel, not forwarded)
{
"client": "BRIMON",
"request_refnum": "123456789154",
"username": "acctest009",
"timestamp": "1652034352767",
"channel_id": "NBMB",
"reference_number": "987654321098",
"request_id": "<overwritten by doom>"
}
Struct: usecase.LoaSubmitRequest (internal/interfaces/usecase/loa.go:131)
Response Body (200)
Response Body 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
}
]
}
}
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".
Response body
"ok"
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.gointernal/usecase/loa/list.gointernal/usecase/loa/form.gointernal/usecase/loa/term.gointernal/usecase/loa/confirm.gointernal/usecase/loa/send_otp.gointernal/usecase/loa/submit_otp.gointernal/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.phperangel/application/controllers/api/v3/loan_on_app/List_loa.phperangel/application/controllers/api/v3/loan_on_app/Form.phperangel/application/controllers/api/v3/loan_on_app/Term.phperangel/application/controllers/api/v3/loan_on_app/Confirm.phperangel/application/controllers/api/v3/loan_on_app/Send_otp.phperangel/application/controllers/api/v3/loan_on_app/Submit_otp.phperangel/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(usesresponse_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