# AutoDCD Implementation and Workflow ## CRED-6159 — 2026-05-04 ## CRED-6224 — 2026-05-26 ## Objective The `AutoDCD` (Disbursement Checklist Done) feature dynamically evaluates if a given application qualifies to be moved internally to the `disbursement checklist done` state automatically immediately after all signers (first_party, application, and optionally co_applicant) complete the Loan Agreement signing cycle. ## Trigger Points ### 1. Loan Agreement Signing Webhook (Admin Backend) The sequence initializes synchronously from `LoanHypothecation::updateHypothecationWebhook()` in `app/LoanHypothecation/LoanHypothecation.php` (which also handles Digio webhooks via `simplifyThedataIfDigio()`). AutoDCD fires **only on applicant / co-applicant signing events** — not on the final first-party signing. The trigger is gated on the `has_all_signed` flag carried in the Digio webhook payload: - When applicant or co-applicant signs, `has_all_signed` is `false` → `AutoDCD::precheckAndCreateJob()` is invoked. - When the first party signs (the last signer), `has_all_signed` becomes `true` → AutoDCD is **not** re-invoked, avoiding duplicate runs. ```php if (!$has_all_signed) { (new ConsumerApplicationController())->webhook($application->id, ['type' => 'Loan-Agreement-Signing-Webhook', 'status' => 'completed', 'data' => []]); \App\Jobs\AutoDCD::precheckAndCreateJob($application->id); } ``` The application state must also be in `approved` or `disbursement query` for the trigger to run. Note: `has_all_signed` is only populated for the Digio integration; legacy Leegality webhooks do not propagate this flag and therefore do not trigger AutoDCD from this path. ### 2. Document Upload Endpoint (API-Wrapper) For specific SSE merchant flows, the AutoDCD job is additionally triggered via the `api-wrapper` through the `/customerRest/documentSrv/uploadDocument` endpoint. To prevent duplicate job queuing, this trigger evaluates two strict gates dynamically during document upload: - **Complement Document Check:** The trigger fires strictly only when *both* the `tfr` and `proforma` (Solar Quotation) documents are present in the `consumer_application_files` table. When one is uploaded, it checks for the existence of the other. - **Duplicate Job Check:** It ensures no pre-existing AutoDCD job (`from = 'autoDCD'`) originating from `sse_doc_upload` is already queued for the application before insertion. Hence, for SSE cases, AutoDCD will typically run twice in total: once upon agreement signing and once when both mandatory documents are uploaded. ## Logic and Flow ### 1. `app/Jobs/AutoDCD.php` - **Class**: `App\Jobs\AutoDCD` - **Responsibility**: Responsible for performing synchronous pre-checks to determine if an application can be processed for AutoDCD (e.g., preliminary verification flags, lender criteria validation). - **Synchronous Pre-checks**: 1. **Project & Customer Types**: Validates `type_of_customer` is `1` and `solar_power_type` is `1` inside `consumer_application_element`. 2. **Net Loan Amount**: Calculates `net_loan_amount` from the `autoBRE` logic and ensures it is strictly less than ₹5,00,000. *(Note: Agreement signing checks for applicants and co-applicants are already validated before triggering this job)* - **Database Entry**: If all synchronous pre-checks pass, it inserts a job object into the `job_providers` table tagged specifically with `'from' => 'autoDCD'`, encoding the gathered preliminary payload data (like values evaluated during checks). If a co-applicant exists, a boolean flag `has_co_applicant` is explicitly passed within this payload to inform downstream checks. ### 2. `app/Jobs.php` The centralized worker file fetches available records queued within `job_providers`. When reaching our specific application ticket matching `autoDCD`: - It processes cases iteratively and delegates logic execution to `AutoDCDController`. ### 3. `app/Http/Controllers/Admin/AutoDCDController.php` - **Class**: `App\Http\Controllers\Admin\AutoDCDController` - **Primary Method**: `process($job, $payload)` - **Responsibility**: Evaluates time-complex validations securely via `checkValidations()`, then moves the application to DCD via `moveToDCD()` if all checks pass. #### `checkValidations()` — Modular Validation Orchestrator This method is a thin orchestrator that delegates each check to a dedicated private method, connected with short-circuit `&&`. A failure in any check stops evaluation of subsequent ones. To add a new validation, add one private method and chain it here. Current checks (in order): --- **1. Aadhaar Validation** — `validateAadhaar()` Verifies that the applicant's `aadhaar_card` is populated and is exactly 12 digits long. If `has_co_applicant` is true, the co-applicant's `co_applicant_aadhaar_card` must also be 12 digits. --- **3. Electricity Bill Date Validation** — `validateEbillDate()` - **Source**: `utility_validation` table (FK: `consumer_application_id`), `data` column (JSON). - **Field checked**: `result.bill_date` — date in `DD-MM-YYYY` format (e.g. `18-04-2026`). - **Rule**: The `bill_date` must not be older than 2 months from today. - **Skip conditions** (check is bypassed, returns `true`): - No row exists in `utility_validation` for the application (Karza bill was never fetched). - `bill_date` key is absent or empty in the JSON. - `bill_date` cannot be parsed as a valid date. - **On failure**: Adds an advisor-tagged comment to the application via `ApiController::updateComments()` (system user 107), visible to customer only (`show_consumer = 1`, `show_merchant = 0`). The advisor tag (`@userId-Name`) is resolved from `consumer_application.created_by` using `WebhookController::getCommentTag()`. Failure comment: > "Auto DCD failed, Do the updated E-bill validation or upload lastest validation document." - Logs failure to CloudWatch and returns `false`. --- **4. E-bill Karza Failure — Fallback Document Check** — `validateEbillSystemFailDoc()` - **Karza failure signal**: The latest `consumer_application_files` row with `file_type IN ('electricity_bill_validation_report', 'electricity_bill')` has `ebill_name_verify IS NULL`, or has `ebill_name_verify` populated but any match percentage is below `60%`. The column is populated by Karza as a JSON object e.g. `{"applicant_match":60,"co_applicant_match":79}` or `{"applicant_match":41}`. - **Skip conditions** (check is bypassed, returns `true`): - No file with those types exists for the application. - The latest file has `ebill_name_verify` populated (Karza ran) **and** all present match percentages (`applicant_match`, `co_applicant_match`) are `>= 60%`. - **When Karza failed**: Checks whether at least one fallback document has been uploaded. Accepted file types (case-insensitive `LIKE` contains match): - `electricity_bill_validation_report` - `utilitybillvalidation` - `ebill payment proof` - `Manual E-bill Validation` - **On failure** (Karza failed AND no fallback doc found): Logs to CloudWatch and returns `false`. No advisor comment is added for this check. --- **5. Bank Validation (Penny Drop)** — `validateBankValidation()` - **Source**: `bank` table (FK: `application_id`, `applicant_type = 1`), `karza_response` column (JSON). - **Success condition**: `result.bankTxnStatus === true` AND `result.bankResponse` is `'Transaction Successful'` or `'SUCCESSFUL TRANSACTION'`. - **This check is compulsory** — if it fails, the mandate/NACH check (check 6) is skipped entirely via short-circuit. - **On failure**: Logs to CloudWatch with the raw `karza_response` for diagnosis and returns `false`. No comment is added. --- **6. E-Mandate or P-NACH + BSV Validation** — `validateMandateOrNach()` Validates that the application has a registered digital mandate **or** a physical NACH with bank statement verification. The two paths are mutually exclusive in practice. - **E-Mandate path (checked first)**: A file exists in `consumer_application_files` with `file_type` containing `'e mandate'` or `'emandate'` (case-insensitive). If found → passes, P-NACH path is not evaluated. - **P-NACH path (evaluated only when E-Mandate file is absent)**: - A NACH file must exist: `file_type LIKE '%nach%'` (case-insensitive). - A BSV document must also exist: `file_type LIKE '%bsv%'` (case-insensitive). - Both must be present for the check to pass. - **CloudWatch log messages** (specific per scenario): - P-NACH file present but BSV missing → `"P-NACH uploaded but BSV document not uploaded"` - Neither E-Mandate nor NACH file found → `"Neither E-Mandate document nor P-NACH uploaded"` - No comment is added to the application on failure. --- **7. Digital KYC & Verification — `validateKycOrOsv()`** Validates that each applicant (and co-applicant, if present) has completed either a Two-way VKYC or has all OSV (Self-Attested) documents approved, including the selfie. Delegates entirely to `PostApprovalController::checkKYCStatus($applicationId)`, which returns a JSON response decoded to determine pass/fail. - **Pass condition**: `applicant.kyc_status === 'approved'` (and `co_applicant.kyc_status === 'approved'` when a co-applicant is present). - **VKYC approved path (either vendor)**: - **Karza**: `karza_video_webhooks.data.approvedByAuditor === 'ACCEPTED'` (via `KarzaVideoAttribute` → `KarzaVideoWebhook` relation). - **Digio**: `digio_vkyc` row with `kyc_journey = 'complete'` and `kyc_status = 'approved'`. - **OSV approved path** — evaluated when VKYC is not approved. Requires all three documents with `doc_verify = 1` in `consumer_application_files`: - Applicant: `self_attested_pan`, `self_attested_aadhar`, `self_attested_selfie` - Co-applicant: `self_attested_co_pan` or `co_app_self_attested_pan`; `self_attested_co_aadhar` or `co_app_self_attested_aadhar`; `self_attested_co_selfie` - **On failure**: Logs to CloudWatch — `"KYC is missing / Self Attested Documents Of OSV is missing for Applicant"` or `"...for Co-Applicant"`. No comment is added. --- --- **9. Advance/DP Receipt — `validateAdvanceDpReceipt()`** Validates that an Advance / Down Payment receipt is present when DP is applicable for the application. - **Skip conditions (Zero DP)**: Returns `true` immediately if: - No row exists in `pro_forma_invoice` for the application, OR - `merchant_collected_advance_amount != 1`, OR - No row in `emi_calculation` OR `upfront_from_customer` is 0/null. - **When DP is applicable**: Passes if **either** is present: - A `consumer_application_files` row with `file_type = 'advance_amount_receipt'` and `deleted_at IS NULL`, OR - A `payment_transactions` row with `payment_for = 'advance_amount'` and `deleted_at IS NULL`. - **On failure**: Logs to CloudWatch — `"Advance Payment Confirmation / Down Payment document is missing"`. No comment is added. --- **10. PF Entry — `validatePfEntry()`** Validates that the processing fee collected by CF (Credit Fair) has a corresponding payment entry covering the full required amount. - **Skip conditions**: Returns `true` immediately if: - No row exists in `pro_forma_invoice` for the application, OR - `processing_fee_by_merchant == 1` (merchant collects PF directly). - **When CF collects PF** (`processing_fee_by_merchant != 1`): Checks that `SUM(amount)` from `payment_transactions` where `payment_for = 'processing_fee'` and `deleted_at IS NULL` is `>= emi_calculation.processing_fee`. - **On failure**: Logs to CloudWatch — `"PF entry is pending"` with `required` and `paid` amounts for diagnostics. No comment is added. --- **11. Required Documents & Elements — `validateRequiredDocuments()`** Validates the presence of mandatory post-sanction files and application elements in a single optimized execution. It fetches all active files from `consumer_application_files` once and evaluates them against a required rule set. If any of the following conditions fail, it immediately logs the corresponding error message to CloudWatch under the `"AutoDCD Validation Failed"` event and returns `false`. **File Validations:** * **Solar Quotation / Final Quotation:** Requires either `file_type = 'solar_quotation'` or `file_type = 'proforma'`. These two documents are complementary, so if either one is present, the other is not reported as missing. (Logs: *"Quotation missing"* / *"Final Quotation / Final Bill missing"* only when neither exists) * **TFR Report:** Requires `file_type = 'tfr'` (case-insensitive). (Logs: *"TRF missing"*) * **CAM:** Requires `file_type = 'credit_appraisal_memo'`. (Logs: *"CAM missing"*) * **Sanction Letter:** Requires `file_type = 'sanction_letter'`. (Logs: *"Sanction letter missing"*) * **Merchant Confirmation:** Requires a file type containing both `'merchant'` and `'confirmation'` or `'pro_forma_invoice'` (case-insensitive) . (Logs: "Merchant Email Confirmation / Signed DA missing") * **Final Quotation:** Covered by the complementary Solar Quotation / Final Quotation rule above. * **Signed Loan Agreement:** This validation is paused for the time being. **Element Validations:** * **Product Name:** Validates that `product_name` is non-empty in the `consumer_application_element` table for this application. (Logs: *"Product Name missing"*) --- **19. Reference Call — `validateReferenceCall()`** Evaluates each reference contact independently using raw DB queries: 1. Loads `reference_contact_1` and `reference_contact_2` from `consumer_application_element`. If neither is recorded: fails. 2. For each contact that is present, the corresponding reference file (`reference_1` or `reference_2`) must exist in `consumer_application_files`. 3. **SSE merchant exception**: If `consumer_application.merchant_id == env('SSE_MERCHANT_ID')`, the `reference_1` / `reference_2` file check is skipped. 4. If both contacts are present, **both** must individually pass. If only one contact is present, only that one must pass. - **On failure**: Logs `"Reference number missing"` to CloudWatch. --- **20. Verification Call — `validateVerificationCall()`** Validates that a verification call recording exists, **only when OSV is presented**. - **OSV is presented** (both conditions must be true): - Applicant VKYC is not approved (`!isVkycApproved($applicationId, 1)`), AND - At least one self-attested document (`self_attested_pan` or `self_attested_aadhar`) exists in `consumer_application_files`. - **Skip**: Returns `true` immediately when OSV is not presented (VKYC was used). - **When OSV presented**: Checks `call_logs` for `purpose = 'verification_call'`, `status = 'completed'`, `recording IS NOT NULL AND recording != ''`. - **On failure**: Logs `"Verification call missing"` to CloudWatch. --- **21. Mandatory Required Docs — `validateMandatoryDocs()`** Validates that every document listed in `consumer_application_element.mandatory_document_list` (a comma-separated string of file_type keys) is present in `consumer_application_files`. - **Skip**: Returns `true` immediately if `mandatory_document_list` is null or empty. - **Matching**: For each key, checks `file_type = $key` OR `file_type = str_replace('_', ' ', $key)` (mirrors checklist page logic). - **Special-case alias**: When the required key is `emandate`, the check also accepts `e mandate` as a valid `file_type` (and vice versa). This is in addition to the underscore→space transform above (which would otherwise leave `emandate` unchanged since it has no underscore). - **On failure**: Logs `"Mandatory is pending"` to CloudWatch with a `missing_docs` array listing all absent documents. --- If all checks return `true`, the application is moved to DCD by merging `state = 'disbursement checklist done'` and `source = 'autoDCD'` into the request, then calling `ConsumerApplicationController::updateStatus($applicationId)` directly. This method handles all state transition steps: 1. **Hard Bureau Pull**: Executes `hardBureauPullForRfd()` automatically for Lender 2 (RFD). If the hard pull fails, `updateStatus()` returns early and the state change is halted. 2. **JobProvider Hook**: Inserts a `statusChange` job into `job_providers` to run downstream triggers, with the full request input (including `source = 'autoDCD'`) included in the payload. 3. **Application Update**: Updates `ConsumerApplication`'s `state` to `disbursement checklist done`. 4. **StatusLogs Tracking**: Records the status transition into `StatusLogs` under the system auto-decision user (`user_id`: 107, resolved via `Auth::user() ?: 107`). 5. **Bank Validation Alert**: If the bank validation response on the application is absent or failed, sends a WhatsApp alert to the relevant team members. ## Result Logging Every AutoDCD validation run is persisted to the `auto_dcd_log` table (model: `App\AutoDCDLog`). Each row records the `application_id`, `job_id`, `triggered_by` source, an overall `status` (`success` / `fail`), and a `result` JSON column containing each validation's outcome (`pass`, `fail`, or `skip`) with a descriptive message. All validations are executed regardless of prior failures so the log captures the complete picture; the only exception is `mandate_or_nach`, which is skipped when `bank_validation` fails. ### Checklist Page UI When an application is in `approved` state, the checklist page (`checklist_page.blade.php`) consults the latest `auto_dcd_log` entry on page load via an AJAX `GET` call to `AutoDCDController@getFailedValidations` (route: `auto-dcd-log/{application_id}`, named `auto-dcd-log.get`) and renders one of two indicators in the heading bar based on the response `status`: - **`fail`** → a red **"AutoDCD Failed. Check the reasons here."** button (`#btnAutoDcdFailures`) is shown. Clicking it opens a modal listing only the failed validations with their reasons, pre-populated from the same response payload (`failures[]`). - **`success`** → a green **"AutoDCD passed but the application state did not move to Disbursement Checklist Done"** badge (`#autoDcdSuccessBadge`) is shown. This surfaces the edge case where all validations passed but the downstream state transition in `ConsumerApplicationController::updateStatus()` did not complete (e.g., hard bureau pull failure or an exception during the status change), leaving the application stuck in `approved`. The badge is non-interactive — its purpose is to alert the advisor that manual intervention is needed. If no `auto_dcd_log` row exists for the application, the response is `{show: false}` and neither indicator is rendered. ### Failure Comments When any validation fails, an advisor-tagged comment is automatically posted on the application via `ApiController::updateComments()` (system user 107), visible to customer only (`show_consumer = 1`, `show_merchant = 0`). The `@advisorTag` embedded in the comment text is parsed by `getPureComment()` to trigger an email notification to the tagged advisor: - **General failure**: _"Auto DCD failed, please check reason on Checklist page"_ — posted after the validation loop whenever `allPassed` is `false`. - **E-bill date failure**: _"Auto DCD failed, Do the updated E-bill validation or upload lastest validation document."_ — posted inside `validateEbillDate()` when `bill_date` is older than 2 months. This results in two comments for e-bill date failures (the specific one + the general one). ## Extending Logic As required systematically moving forward, any further pre-checks can safely be pushed to `AutoDCD::precheckAndCreateJob` without interfering with `handleWebhook()`. Expansive validations up until the Disbursement Checklist Done movement should be added inside `AutoDCDController::checkValidations()` — add a new private method, register it in the `$validations` array inside `checkValidations()`, and set `$this->lastFailureReason` before returning `false` so the log captures the failure reason.