# AutoDCD Implementation and Workflow ## CRED-6159 — 2026-05-04 ## CRED-6224 — 2026-05-26 ## Overview The AutoDCD (Disbursement Checklist Done) feature is an automated system designed to streamline the loan approval and disbursement process by eliminating the need for manual reviews on standard applications. Once a borrower (and co-borrower, if applicable) successfully signs their loan agreement and all mandatory documents are uploaded, the system automatically runs a comprehensive series of business rule checks in the background. It ensures that the loan amount is within the approved limits and that the borrower falls under the correct customer category. The system also verifies that required core documents, such as the Technical Feasibility Report, Sanction Letter, and Solar Quotations, have been properly collected. Additionally, it performs crucial operational checks, such as confirming that the merchant's processing fee has been fully received, verifying the provided e-mandate and bank account details, and validating the reference contacts provided by the applicant. It also ensures that the first EMI payment dates and dates of birth are correctly aligned with our internal policies. If an application successfully passes every single one of these automated checks, the system automatically advances its state to "Disbursement Checklist Done," effectively greenlighting it for final payout without any human intervention. Conversely, if any check fails, the system halts the automated progression, logs the exact reasons for the failure directly on the advisor's dashboard, and flags the application so the operations team can step in to manually resolve the missing information. ## 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. *Note:* `precheckDocuments` ensures that mandatory documents (Solar Quotation, TFR, Sanction Letter, Merchant Confirmation/Signed DA, Signed Loan Agreement) are uploaded before allowing the background job to be queued. 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. ### 3. General Document Uploads and System Document Generation To ensure AutoDCD handles cases where mandatory documents are uploaded or generated *after* the loan agreement is signed, a universal helper method `AutoDCDController::tryTriggerAutoDCD($applicationId)` is used. This helper validates the following gates before queuing the AutoDCD job: 1. **No Duplicate Execution:** Ensures no record exists for this application in the `auto_dcd_log` table. 2. **Agreement Signed:** `applicant_signing` (and `co_applicant_signing` if applicable) must be completed in the `loan_hypothecation_model`. 3. **Mandatory Documents Present:** Reuses `precheckDocuments` to verify 2 core required documents are uploaded (Solar Quotation and TFR). **Hook Locations:** * **Manual Uploads:** At the end of `ConsumerApplicationController::uploadConsumerDocuments()` (after explicitly validating the application state is `approved` or `disbursement query`). ## 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. 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`. --- **2. 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. --- **3. 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. --- **4. 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. --- **5. 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. --- --- **6. 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. --- **7. 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. --- **8. Required Documents & Elements — `validateRequiredDocuments()`** Validates the presence of mandatory application elements and some document files that are checked dynamically rather than in the upstream precheck. If any of the following conditions fail, it immediately logs the error message to CloudWatch under the `"AutoDCD Validation Failed"` event and returns `false`. **File Validations:** * **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'`. (Logs: *"Merchant Email Confirmation / Signed DA missing"*) **Element Validations:** * **Product Name:** Validates that `product_name` is non-empty in the `consumer_application_element` table for this application. (Logs: *"Product Name missing"*) --- **9. Reference Call — `validateReferenceCall()`** Evaluates reference contacts using raw DB queries: 1. Loads `reference_contact_1` and `reference_contact_2` from `consumer_application_element`. If neither is recorded: fails. 2. At least one valid reference file (`reference_1` or `reference_2`) corresponding to a recorded contact 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. - **On failure**: Logs `"Reference number missing"` or `"At least one reference document is missing"` to CloudWatch. --- **10. 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.