# 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": "", "id": "", "desc": "success", "data": { ... endpoint-specific payload ... } } ``` When an error occurs, `data` is wrapped as `{"error": ""}` (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.
Request Body JSON ```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 ```json { "code": "00", "refnum": "123456789154", "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 } ] } ] } } } ```
`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`:
Application item schema ```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 } ] } ```
**`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.
Request Body JSON ```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.LoaListRequest` (`internal/interfaces/usecase/loa.go:186`) ### Response Body (200)
Response Body JSON ```json { "code": "00", "refnum": "123456789154", "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 } ] } } ```
`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 |
Frontend → Erangel ```json { "card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0" } ```
Erangel → Doom (constructed in Doom.php:81) ```json { "client": "BRIMON", "request_refnum": "123456789154", "username": "acctest009", "timestamp": "1652034352767", "channel_id": "NBMB", "card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0", "request_id": "" } ```
Struct: `usecase.LoaFormRequest` (`internal/interfaces/usecase/loa.go:20`) ### Response Body (200)
Response Body JSON ```json { "code": "00", "refnum": "123456789154", "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 } } ```
`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 |
Frontend → Erangel ```json { "card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0", "amount": 5000 } ```
Erangel → Doom (constructed in Doom.php:95) ```json { "client": "BRIMON", "request_refnum": "123456789154", "username": "acctest009", "timestamp": "1652034352767", "channel_id": "NBMB", "card_token": "f104d035a92652987b8c82dd91cb13aa1f403d686dcbc3844d261cc8416de9b0", "amount": 5000, "request_id": "" } ```
Struct: `usecase.LoaTermRequest` (`internal/interfaces/usecase/loa.go:104`) ### Response Body (200)
Response Body JSON ```json { "code": "00", "refnum": "123456789154", "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 } ] } } ```
`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 |
Frontend → Erangel ```json { "otp_type": "WA" } ```
Erangel → Doom ```json { "client": "BRIMON", "request_refnum": "123456789154", "username": "acctest009", "timestamp": "1652034352767", "channel_id": "NBMB", "otp_type": "WA", "request_id": "" } ```
Struct: `usecase.LoaSendOtpRequest` (`internal/interfaces/usecase/loa.go:177`) ### Response Body (200)
Response Body JSON ```json { "code": "00", "refnum": "123456789154", "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 | ### 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 |
Frontend → Erangel ```json { "server_id": "abc123", "otp": "123456", "otp_type": "WA" } ```
Erangel → Doom ```json { "client": "BRIMON", "request_refnum": "123456789154", "username": "acctest009", "timestamp": "1652034352767", "channel_id": "NBMB", "server_id": "abc123", "otp": "123456", "otp_type": "WA", "request_id": "" } ```
Struct: `usecase.LoaSubmitOtpRequest` (`internal/interfaces/usecase/loa.go:TBD`) ### Response Body (200)
Response Body JSON ```json { "code": "00", "refnum": "123456789154", "id": "", "desc": "success", "data": null } ```
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 |
Frontend → Erangel ```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" } ```
Erangel → Doom (pin consumed by erangel, not forwarded) ```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": "" } ```
Struct: `usecase.LoaSubmitRequest` (`internal/interfaces/usecase/loa.go:131`) ### Response Body (200)
Response Body JSON ```json { "code": "00", "refnum": "123456789154", "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 } ] } } ```
`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 ```json "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.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`