From 81a3b8d0c9c1b863b59af76ebfe8ed6a44701d65 Mon Sep 17 00:00:00 2001 From: Achmad Setyabudi Susilo Date: Thu, 25 Jun 2026 14:13:24 +0700 Subject: [PATCH] Add README --- README.md | 978 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 978 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..99f8958 --- /dev/null +++ b/README.md @@ -0,0 +1,978 @@ +# 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. + +```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) + +```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`: + +```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. + +```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) + +```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 | + +```json +// 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": "" +} +``` + +Struct: `usecase.LoaFormRequest` (`internal/interfaces/usecase/loa.go:20`) + +### Response Body (200) + +```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 | + +```json +// 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": "" +} +``` + +Struct: `usecase.LoaTermRequest` (`internal/interfaces/usecase/loa.go:104`) + +### Response Body (200) + +```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 | + +```json +// Frontend → Erangel +{ + "otp_type": "WA" +} + +// Erangel → Doom +{ + "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) + +```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 | + +```json +// Frontend → Erangel +{ + "server_id": "abc123", + "otp": "123456", + "otp_type": "WA" +} + +// Erangel → Doom +{ + "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) + +```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 | + +```json +// 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, + "pin": "123456" +} + +// Erangel → Doom (pin consumed by erangel, not forwarded) +{ + "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) + +```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"`. + +```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`