# SolarSquare mConnect Webhook Events ## Objective This document explains how the merchant mConnect webhook system works, how to add a new event for the SolarSquare webhook, and how to decide whether the new flow needs only a `webhook()` trigger or a full Laravel event/listener/command job. SolarSquare is identified in this repo by `SSE_MERCHANT_ID` in `.env` (`6990` in the current local environment). The webhook row named `SolarSquare Webhook` is only a label in the `webhook` table. Runtime delivery is controlled by: - `webhook.merchant_id` - `webhook.endpoint` - `webhook.status` - `webhook.events` The code never checks the webhook row name. --- ## Existing Data Model ### Merchant webhook subscription Table: `webhook` Important columns: | Column | Meaning | |--------|---------| | `name` | Admin-facing label only, for example `SolarSquare Webhook` | | `merchant_id` | Merchant that owns this webhook subscription | | `endpoint` | External URL where CreditFair POSTs the webhook payload | | `status` | Active/inactive flag. Only active rows are posted to | | `events` | Comma-separated list of event names/status names this endpoint listens to | The table does not store payload schemas. It stores subscriptions. ### Request/response logs Table: `webhook_request_logs` Every outbound merchant webhook attempt is logged here with: - `request_id` - `webhook_id` - `request` - `response` - `application_id` --- ## Shared mConnect Payload Shape All merchant webhook events eventually go through: `App\Http\Controllers\Admin\ConsumerApplicationController::webhook($application_id, $data)` That method builds the common payload envelope: ```json { "returnCode": "000000", "applicationNo": "CF12345", "type": "Application-Webhook", "status": "ready for disbursement", "data": {}, "uniqueIdNumber": "optional" } ``` Field meanings: | Field | Meaning | |-------|---------| | `returnCode` | Hardcoded success marker: `000000` | | `applicationNo` | Uppercase loan application/agreement number | | `type` | Broad event type, for example `Application-Webhook`, `VKYC-Webhook` | | `status` | Event status, for example `approved`, `completed`, `ready for disbursement` | | `data` | Event-specific payload | | `uniqueIdNumber` | Optional, added from `consumer_application_element.unique_id_number` when present | --- ## Shared mConnect Delivery Flow Whenever any code calls: ```php (new ConsumerApplicationController())->webhook($applicationId, [ 'merchant_id' => _env('SSE_MERCHANT_ID'), 'type' => 'Some-Webhook', 'status' => 'completed', 'data' => ['key' => 'value'], ]); ``` the following happens: 1. `ConsumerApplicationController::webhook()` resolves the merchant id. 2. It loads all `webhook` rows for that merchant. 3. It checks whether either of these values appears in the row's comma-separated `events`: - `$data['status']` - `$data['type']` 4. If matched, it builds the payload envelope. 5. It fires `EventMConnectWebhook`. 6. `ListenerMConnectWebhook` writes a queue log for `command:mConnect` with `con = mConnectWebhook`. 7. `MConnect` command calls `MerchantApiIntegration::push()`. 8. `push()` posts the JSON payload to the webhook endpoint using cURL. 9. The request/response is logged in `webhook_request_logs`. Important: event matching is an OR condition. A row listening to `VKYC-Webhook` receives all VKYC statuses because `type = VKYC-Webhook`. A row listening only to `approved` receives any webhook whose `status = approved`, regardless of `type`. --- ## When To Create Only A New mConnect Event Use this path when the business logic already runs somewhere and you only need to notify SolarSquare. You do not need a new Laravel `Event`, listener, command, or job. Add one call to `webhook()` at the correct business point. ### Steps 1. Pick the event contract. Example: ```php $type = 'Solar-Installation-Webhook'; $status = 'completed'; $data = [ 'installationDate' => $installationDate, 'installerName' => $installerName, ]; ``` 2. Add the event name to the SolarSquare webhook row. If SolarSquare should receive every status under this event type, add: ```text Solar-Installation-Webhook ``` If SolarSquare should receive only one status across any event type, add: ```text completed ``` Prefer event type names over generic statuses, because generic statuses can match unrelated flows. 3. Trigger the webhook from the business code: ```php (new ConsumerApplicationController())->webhook($applicationId, [ 'merchant_id' => _env('SSE_MERCHANT_ID'), 'type' => 'Solar-Installation-Webhook', 'status' => 'completed', 'data' => [ 'installationDate' => $installationDate, 'installerName' => $installerName, ], ]); ``` 4. Verify: - `webhook.events` contains `Solar-Installation-Webhook`. - `webhook.status = 1`. - `webhook.endpoint` is reachable. - A row is created in `webhook_request_logs`. - Slack logs under the mConnect group show the cURL response. --- ## Adding A New Event To The Admin Dropdown The merchant API UI gets event options from `ConsumerApplicationController`. Application statuses come from `$this->status`. Other event types come from: ```php $this->otherWebhookEvents = [ 'utr', 'ntc', 'Merchant-Comment', 'Add-Co-applicant', 'Paytm-Webhook', 'VKYC-Webhook', 'Loan-Agreement-Signing-Webhook', 'NACH-Webhook', 'DA-Confirmation-Webhook', 'JSON-BRE', 'Document-Verification-Webhook', 'Mandatory-Document-Webhook' ]; ``` If the new event should be selectable in the UI, add the new event type here: ```php 'Solar-Installation-Webhook' ``` Then open: ```text /admin/merchant/api/{SSE_MERCHANT_ID} ``` Edit `SolarSquare Webhook` and select the new event. --- ## Worked Example: Loan Agreement Signing Webhook Using Digio This is the existing flow for: ```text Loan-Agreement-Signing-Webhook ``` The SolarSquare webhook receives this event if its `events` column contains `Loan-Agreement-Signing-Webhook`. ### Business meaning CreditFair sends SolarSquare a webhook when the loan agreement signing process is fully completed and the signed loan agreement document has been downloaded/saved. ### Final outbound payload ```json { "returnCode": "000000", "applicationNo": "CF12345", "type": "Loan-Agreement-Signing-Webhook", "status": "completed", "data": {}, "uniqueIdNumber": "optional" } ``` ### Digio signing flow 1. Digio sends the signing callback to CreditFair. 2. Route: ```php Route::post('webhook/digio-signing', 'ApiController@getLeegalityWebhook'); ``` The route reuses `getLeegalityWebhook()` even for Digio signing. 3. `ApiController::getLeegalityWebhook()` receives the payload and fires: ```php event(new EventHypothecation([ 'webhook' => true, 'data' => $request->all() ])); ``` 4. `ListenerHypothecation` converts that event into a queue command: ```php 'cmd' => 'command:LoanHypothecation', 'con' => 'jobs', 'param' => $event->param ``` 5. `LoanHypothecationWebhook` command receives the queued job and calls: ```php (new LoanHypothecation(new Request($param)))->updateHypothecationWebhook(); ``` 6. `LoanHypothecation::updateHypothecationWebhook()` normalizes the callback payload with: ```php $response = $this->simplifyThedataIfDigio(); ``` 7. It identifies the application from the normalized payload: ```php ConsumerApplication::where('id', $response['data']['irn'])->first(...) ``` 8. It checks whether the signing action is successful: ```php $is_action_signed = isset($response['data']['request']['action']) && $response['data']['request']['action'] == "Signed"; $is_error = isset($response['data']['request']['error']) && $response['data']['request']['error'] != null; ``` 9. For Digio payloads, it checks: ```php $has_all_signed = isset($response['data']['has_all_signed']) && $response['data']['has_all_signed'] == 1; ``` 10. If all signers have signed, it updates final-party signing and downloads the signed document: ```php $this->updateHypothecation($application->id, $loanHypothecationModel->id, 'first_party'); $this->fetchDocuments($application->id, $application->lender_id, $loanHypothecationModel->documentId, $response); ``` 11. `fetchDocuments()` detects the Digio payload: ```php if (isset($response['data']['digio'])) { $host = rtrim(_env('DIGIO_BASE_URL', 'https://api.digio.in'), '/') . '/v2/client/document/download'; $this->url = $host . '?document_id=' . $documentId; $this->digio = true; } ``` 12. It calls: ```php $this->process(); ``` This downloads the signed document. 13. Because `$this->digio = true`, `process()` calls: ```php return $this->handleDigioResponse($response); ``` 14. `handleDigioResponse()` saves the signed loan agreement: ```php $this->saveLoanAgreement($responseObj); ``` 15. For the signed document download call, `digio_audit_trail` is false, so it sends the mConnect webhook: ```php (new ConsumerApplicationController())->webhook($this->application_id, [ 'type' => 'Loan-Agreement-Signing-Webhook', 'status' => 'completed', 'data' => [] ]); ``` 16. Since the application belongs to SolarSquare and SolarSquare's `webhook.events` contains `Loan-Agreement-Signing-Webhook`, `ConsumerApplicationController::webhook()` matches the row. 17. The shared mConnect delivery flow runs: ```text ConsumerApplicationController::webhook() -> EventMConnectWebhook -> ListenerMConnectWebhook -> command:mConnect -> MerchantApiIntegration::push() -> POST SolarSquare endpoint -> webhook_request_logs ``` 18. After signed document download, `fetchDocuments()` also downloads the Digio audit trail by setting: ```php $this->digio_audit_trail = true; $this->process(); ``` The mConnect webhook is not sent for the audit trail call because `handleDigioResponse()` only sends it when `digio_audit_trail` is false. --- ## Checklist For Creating A New SolarSquare Webhook Event 1. Define event type and statuses. 2. Decide whether the flow needs async processing. 3. If not async, add a direct `ConsumerApplicationController::webhook()` call at the business trigger point. 4. If async, create event, listener, command, register them, and call `webhook()` from the command/processor. 5. Add the event type to `ConsumerApplicationController::$otherWebhookEvents` if it should appear in the admin UI. 6. Add the event type to the `SolarSquare Webhook` row's `events`. 7. Confirm the application belongs to SolarSquare (`merchant_id = SSE_MERCHANT_ID`). 8. Trigger the business flow in staging. 9. Confirm `webhook_request_logs` has the request/response. 10. Confirm SolarSquare received the payload and returned HTTP 200. --- ## Common Mistakes - Adding a value to `$otherWebhookEvents` only makes it selectable. It does not trigger anything. - Adding a value to `webhook.events` only subscribes the endpoint. It does not trigger anything. - Creating a Laravel event/listener/command only creates upstream processing. The merchant webhook still needs an explicit call to `ConsumerApplicationController::webhook()`. - Listening to a generic status like `completed` can unintentionally match multiple webhook types. - Forgetting `webhook.status = 1` prevents delivery. - Expecting the webhook `name` to affect code behavior. It does not.