# WhatsApp BOT AI — Enhancement & Improvement **Jira:** CRED-6153 **Date:** 2026-04-15 --- ## Overview Two commits extend the WhatsApp bot integration (`ServiceController`) to support: 1. **SOA & Payment Link delivery** via App ID, Mobile Number, or Loan Application Number (KMGC cases only) 2. **Mandatory field enforcement** on case login (Name, Mobile, PAN) 3. **Automatic `in-process` state** on new case creation 4. **Status-based auto-reply to merchant WhatsApp groups** whenever a case changes stage 5. **Merchant comment forwarding** to WhatsApp groups when a comment is marked visible to merchants All notifications flow from PHP → narad-bridge (`/_admin/send-group-status` and `/_admin/send-group-comment`) and are delivered to the merchant's mapped WhatsApp group. --- ## What Changed (Summary) | File | Change Type | Description | |------|-------------|-------------| | `app/Http/Controllers/Service/ServiceController.php` | Modified | New endpoints + group notification methods | | `app/Http/Controllers/Admin/ConsumerApplicationController.php` | Modified | Hooks `sendGroupStatusNotification()` into status changes | | `app/Http/Controllers/ApiController.php` | Modified | Hooks `sendGroupCommentNotification()` into comment saves | | `routes/service.php` | Modified | Two new GET routes for SOA and payment link | --- ## Detailed Changes ### 1. New Routes **File:** `routes/service.php` ```php Route::get('account-statement/{id}', 'Service\ServiceController@getAccountStatementByIdentifier'); Route::get('get-payment-link/{id}', 'Service\ServiceController@getPaymentLink'); ``` Both routes sit inside the existing `throttle:60,1` middleware group and use the same `CF_X_CLIENT_ID` / `CF_X_CLIENT_SECRET` header authentication as other service routes. --- ### 2. Mandatory Fields on Case Login **File:** `app/Http/Controllers/Service/ServiceController.php` — `createLoan()` **Before:** Only `pan_card` and `mobile_number` were required to create a new application. **After:** All three fields are required: ```php if (empty($this->parsedData['pan_card']) || empty($this->parsedData['mobile_number']) || empty($this->parsedData['name'])) { return response()->json(['id' => null, 'message' => 'Please share valid Name, PAN Card, and Mobile Number to create application'], 400); } ``` --- ### 3. Automatic `in-process` State on Creation **File:** `app/Http/Controllers/Service/ServiceController.php` — `createLoan()` New applications created via the bot are now set to `in-process` immediately, instead of waiting for all fields to be filled before transitioning: ```php $application->state = 'in-process'; $application->save(); ``` A `StatusLogs` entry is inserted on both create and update paths so the stage transition is recorded: ```php StatusLogs::insert([ 'application_id' => $application->id, 'user_id' => 107, // WhatsApp bot service user 'status' => $application->state, 'created_at' => Carbon::now()->toDateTimeString() ]); ``` The `form completed` auto-transition that previously fired when `checkMandatoryFields()` returned empty has been removed — the bot response message now reflects missing fields as guidance text instead. The response payload now also includes `merchant_slug` so narad-bridge can construct a merchant-specific consent link: ```php return response()->json(['id' => $application->id, 'message' => $message, 'merchant_slug' => $merchant_slug], 200); ``` --- ### 4. SOA & Payment Link Endpoints Both endpoints share a private `resolveApplication()` helper that supports three identifier types via the `id_type` query parameter. #### 4.1 `resolveApplication($value, $idType, $merchant_id)` Resolves a `ConsumerApplication` (columns: `id`, `lender_id`) from the merchant's portfolio: | `id_type` | Resolution Logic | |-----------|-----------------| | `application_id` (default) | Direct lookup by `id` | | `mobile_number` | Strips leading `91` prefix if 12 digits, looks up most recent application by `mobile_number` | | `loan_application_number` | Looks up `ProFormaInvoice.loan_application_no`, then resolves the linked application | All lookups filter by `merchant_id` (from group identifier) and `is_deleted IS NULL`. #### 4.2 `getAccountStatementByIdentifier($value)` — `GET /account-statement/{id}` - KMGC-only (`lender_id = 2`). Returns `403` for other lenders. - Delegates to `ConsumerApplicationController::getAccountStatement($application->id)` which generates/streams the PDF. - Returns `401` if the calling group is not whitelisted, `404` if no application found. #### 4.3 `getPaymentLink($value)` — `GET /get-payment-link/{id}` - KMGC-only. Returns a non-error informational message for other lenders. - Calls `FrontController::getFeesData($application->id)` and extracts `total_payment_link`. - Returns `null` in `data.total_payment_link` (HTTP 200) when there is no pending payment. --- ### 5. Status-Based Group Notifications #### 5.1 Trigger — `ConsumerApplicationController::changeApplicationStatus()` **File:** `app/Http/Controllers/Admin/ConsumerApplicationController.php` Immediately after a status change is validated and before the rejection/approval sub-flows run, the bot notification is fired non-blocking: ```php $groupNotificationStatuses = [ 'incomplete form', 'form completed', 'in-process', 'waiting for documents', 'waiting for decision', 'pre approved', 'cf approved', 'approved', 'rejected' ]; if (in_array(strtolower($status), $groupNotificationStatuses)) { (new ServiceController(new Request()))->sendGroupStatusNotification($application_id, $status); } ``` Any exception is swallowed so the existing status-change flow is never interrupted. #### 5.2 `sendGroupStatusNotification($application_id, $status)` **File:** `app/Http/Controllers/Service/ServiceController.php` **Once-per-stage guard:** For every status except `waiting for documents`, the method checks `StatusLogs` for a prior entry with the same status on the same application. If more than one prior entry exists it returns early, preventing duplicate notifications. ``` waiting for documents → always sends (documents may change between requests) all other statuses → skipped if StatusLogs already has > 1 entry for that status ``` **Group lookup:** Loads the merchant-to-group mapping from S3 (`getGroups()`). Iterates the map to find the group name whose `merchant_id` matches `$application->merchant_id`. Returns early if no match. **Message construction by status:** | Status | Message appended | |--------|-----------------| | `waiting for decision` | "Case is under review with Credit Team, we will provide decision soon." | | `rejected` | "Case is rejected, Does not meet policy criteria" | | `approved` | Post-approval eSign link fetched via `ConsumerApplicationController::getMConnectPayload($id, 'approved')` and appended as `Post Approval Link: ` | | `waiting for documents` | Pending document list fetched via `getMConnectPayload($id, 'waiting for documents')`, formatted as a bulleted list | | all other statuses | Base message only: `Application ID: X\nStatus: Y` | **Delivery:** POSTs to `NARAD_HOST/_admin/send-group-status` with `CF_ACCESS_KEY` / `CF_SECRET_KEY` auth headers. cURL errors are logged to CloudWatch under title `GroupStatusNotificationError`. --- ### 6. Merchant Comment Forwarding to WhatsApp #### 6.1 Trigger — `ApiController` (comment save) **File:** `app/Http/Controllers/ApiController.php` After a comment is saved, if it is marked visible to merchants: ```php if ($comment->show_merchant == 1 || $comment->type == 'merchant') { (new ServiceController(new Request()))->sendGroupCommentNotification( $comment->application_id, $cleanOriginalComment, $username->name ); } ``` Non-blocking — any exception is swallowed. #### 6.2 `sendGroupCommentNotification($application_id, $comment, $commenter_name)` **File:** `app/Http/Controllers/Service/ServiceController.php` Follows the same group-lookup pattern as `sendGroupStatusNotification` (loads S3 groups, matches merchant). POSTs to `NARAD_HOST/_admin/send-group-comment`. cURL errors logged under `GroupCommentNotificationError`. --- ## Flow Diagrams ### Case Creation via WhatsApp Bot ``` Incoming message: "new application ..." │ ▼ createLoan() [ServiceController] │ ├── Validate: Name + Mobile + PAN present? │ NO → return 400 with guidance message │ YES ↓ ├── Check duplicate customer │ FOUND → return 200 "Duplicate Customer found" │ NOT FOUND ↓ ├── Create ConsumerApplication (state = 'in-process') ├── Insert StatusLogs entry ├── Create ConsumerApplicationElement ├── checkMandatoryFields() → guidance message └── Return { id, message, merchant_slug } │ ▼ (narad-bridge appends consent link using merchant_slug) ``` ### Status Change → WhatsApp Group Notification ``` changeApplicationStatus($application_id, $status) │ ▼ status in notification list? NO → skip YES ↓ sendGroupStatusNotification($application_id, $status) │ ├── Once-per-stage guard (skip if status seen > 1 time, except WFD) ├── Load merchant → find group name from S3 map ├── Build message (base + status-specific context) │ waiting for decision → "Case is under review..." │ rejected → "Does not meet policy criteria" │ approved → fetch + append eSign link │ waiting for docs → fetch + append pending docs list └── POST to NARAD_HOST/_admin/send-group-status ``` ### Merchant Comment → WhatsApp Group ``` ApiController: comment saved with show_merchant = 1 / type = 'merchant' │ ▼ sendGroupCommentNotification($application_id, $comment, $commenter_name) │ ├── Load merchant → find group name from S3 map └── POST to NARAD_HOST/_admin/send-group-comment ``` ### SOA / Payment Link Request via WhatsApp ``` Group message: "account statement 98765" / "payment link GDS0285886" │ (narad-bridge detects intent + extracts identifier) ▼ GET /account-statement/{value}?id_type=...&group_identifier=... GET /get-payment-link/{value}?id_type=...&group_identifier=... │ ├── getMerchantByGroup() → authenticate group ├── resolveApplication() → find app (by app_id / mobile / LAN) ├── lender_id == 2 check → KMGC only └── getAccountStatement() / getFeesData() → stream PDF / return link ``` --- ## Business Rules - **SOA and payment link** are only available for KMGC (`lender_id = 2`) cases. - **Identifier resolution priority** (shared across SOA and payment link): mobile number (10 digits) → Loan Application Number (alphanumeric) → Application ID (5–6 digits). - **Group matching** uses the S3-backed merchant-to-group map (`getGroups()`). A merchant not in the map receives no WhatsApp notification. - **Status notifications** fire once per stage transition (deduped via `StatusLogs`), except `waiting for documents` which always fires to reflect the current pending document list. - **Comment notifications** fire only when `show_merchant = 1` or `type = 'merchant'`. --- ## Environment Variables Required | Variable | Used By | Purpose | |----------|---------|---------| | `NARAD_HOST` | `ServiceController` | Base URL of the narad-bridge service | | `NARAD_CF_ACCESS_KEY` | `ServiceController` | Auth header for narad-bridge admin routes | | `NARAD_CF_SECRET_KEY` | `ServiceController` | Auth header for narad-bridge admin routes | --- ## Testing Notes - Creating an application without `name` should return HTTP 400 with the new error message. - New applications created via bot should have `state = 'in-process'` and a matching `status_logs` row. - Moving a case to `waiting for decision` should post a message to the merchant's WhatsApp group with the credit team review text. - Moving a case to `rejected` should post the "Does not meet policy criteria" message. - Moving a case to `approved` should post the eSign/post-approval link (if available). - Moving a case to `waiting for documents` should post a bulleted list of pending documents; re-triggering should send again (no dedup for this status). - Saving a merchant-visible comment should forward it to the WhatsApp group. - `GET /account-statement/{app_id}` for a non-KMGC case should return HTTP 403. - `GET /get-payment-link/{app_id}` for a non-KMGC case should return HTTP 200 with the informational message. - Requesting SOA or payment link using `id_type=mobile_number` and `id_type=loan_application_number` should resolve the correct application.