# Risk-Based ROI Loading, Risky Pincode, and Automated PF/ROI Tenure Gating --- ## CRED-6206 — 2026-07-24 ### Overview Risk-based pricing lets Credit Fair apply automated loan pricing adjustments when a merchant has opted into the feature. A single merchant setting, `pf_surcharge_active`, enables all automated risk pricing: bureau-score-based PF surcharge, bureau-score-based ROI loading, and risky-pincode ROI loading. The system always starts from the selected merchant offer's base PF and base ROI. It independently determines whether the application qualifies for an automated risky PF surcharge and whether it qualifies for automated ROI loading. The raw tenure of the selected offer is used only when both automated charges are eligible. In that dual-eligibility case, an offer of 12 months or less receives automated PF and an offer longer than 12 months receives automated ROI. If only automated PF is eligible, it applies regardless of tenure. If only automated ROI is eligible, it applies regardless of tenure. This rule uses the offer's configured tenure and does not reduce tenure for advance EMIs. Manual PF and manual ROI are intentionally outside this tenure rule, so an authorized user may still apply either or both manual adjustments at any tenure. Automated ROI can be driven by either a bureau-score band or a risky pincode, and when both are eligible the lower non-zero loading is added to the base offer ROI. A manual ROI override takes precedence over automated ROI and is never mixed into automated ROI metadata. The generated EMI plan records the individual bureau and pincode loadings plus the final applied automated ROI loading, so the decision remains auditable after configuration changes. Table of Offers evaluates each candidate with its own pricing, but candidate and final-preview calculations are read-only: they do not update the active EMI row, lock record, or PF surcharge comment. Once BRE selects the final offer, the system persists that offer's total PF, total ROI, and all six automated-risk components together, then updates the offer and raw tenure on the application. This ensures the active EMI row and lock describe only the applied offer rather than an offer considered during eligibility. Approval-time deviations record every eligible automated ROI source: bureau and pincode each receive their own deviation when both were used, including equal-load cases. --- ### Business Rules #### Merchant and source eligibility * **Unified merchant gate**: `merchant.pf_surcharge_active` is the global gate for automated risky PF, bureau ROI, and risky-pincode ROI. It does not disable manually entered PF or ROI changes. * **Bureau PF eligibility**: Automated PF continues to use the configured active bureau-score band and the existing PF bureau-pull-date gate (`2024-08-09`). * **Automated ROI eligibility (Global Date Gate)**: The entire Automated ROI module (Bureau ROI, Pincode ROI, and the Zero-ROI Penalty) strictly applies only to applications with a bureau pull date on or after `2026-08-10 00:00:00`. * **Bureau ROI classification**: Bureau ROI uses an active ROI bureau-score band. * **Pincode ROI classification**: Current-address and installation-address pincodes are checked for risky-pincode classification. Pincode risk affects pricing only; it does not change AutoDecision/BRE outcomes or application status. * **Zero-ROI Pincode Penalty**: Offers with a base `0%` interest rate do not receive automated ROI loadings. If an application selects a `0%` ROI offer and matches a risky pincode, automated ROI is suppressed and a `1%` PF surcharge penalty is evaluated instead. #### Automated eligibility and tenure gate PF and ROI eligibility is determined independently before tenure is evaluated. | Automated PF eligible | Automated ROI eligible | Final automated result | | --- | --- | --- | | No | No | Neither automated charge applies. | | Yes | No | Automated PF applies at any offer tenure. | | No | Yes | Automated ROI applies at any offer tenure. | | Yes | Yes | `<= 12` months: automated PF; `> 12` months: automated ROI. | * **Note on Zero-ROI Offers**: For `0%` ROI offers on risky pincodes, the automated ROI is entirely suppressed. The application falls into the "Automated PF applies at any offer tenure" bucket because the `1%` PF penalty makes automated PF eligible. * The selected `MerchantOffers` record is the source of truth while pricing is calculated. `consumer_application.tenure` and `emi_calculation.remaining_emi` are not used to select automated PF versus automated ROI. * `emi_calculation.tenure` stores the generated plan's raw offer tenure and is a persisted consistency value, not the calculation-time source. * Advance EMIs do not affect the 12-month decision when both automated charges are eligible. #### Manual pricing and ROI priority * **Manual PF is unrestricted**: The manual PF adjustment stored in `risk_based_pricing_calculation` is applied according to its existing increase/decrease rules at every tenure, including when automated PF is suppressed for a long offer. * **Manual ROI is offer-specific**: A valid manual ROI override stored in `risk_based_interest_rate_changes` applies strictly to the specific offer it was created for (at any tenure). It takes precedence over the base and automated ROI for that offer, but will not carry over to or overwrite the pricing of a different offer selected by the Table of Offers. * **No tenure-driven deletion of manual data**: Offer changes do not soft-delete, reset, or otherwise alter manual PF/ROI records. * **Protection of automated calculations**: Generating an EMI with an automated ROI does not falsely insert a manual override row. If a user removes a manual override during EMI generation, the override is deleted and the system dynamically recalculates and restores the correct automated ROI, rather than forcefully reverting to the base offer rate. * **Automated ROI priority math**: If bureau and pincode ROI loadings are both positive, use the maximum loading. If just one is positive, use it. The selected loading is `applied_roi_percentage`. * **Automated PF priority math**: If both a bureau score PF surcharge and the `1%` Zero-ROI Pincode Penalty are applicable, the final PF surcharge applied is the maximum of the two percentages. #### Role-Based Access Control (RBAC) * **Merchant Activation Gating**: The "Activate Risk Based Pricing" checkbox on the merchant edit page is gated by the `Merchant_Activate_Risk_Based_Pricing_Yes|1-0-10-0` permission. Users without this permission will see a disabled checkbox with an explanatory tooltip. * **Bureau Configuration Gating**: On the Risk Based Pricing dashboard, the ability to add new bands, delete existing bands, or toggle a band's status, PF dependency, or ROI dependency is gated by the `Bureau_Configuration_Update|-2-0` (`Risk Based Pricing` -> `Bureau Configuration Update`) permission. * **Risky Pincode Gating**: Updating a category's risky pincode ROI or toggling its status is gated by the `Risky_Pincode_Update|-2-1` (`Risk Based Pricing` -> `Risky Pincode Update`) permission. * **Backend Validation**: The `RiskController@updateRiskBasedPricing` endpoint enforces these dashboard permissions on the server side by silently removing unauthorized segments (`data` and `deleted_band_ids` for bureau configs; `merchant_category_id` and pincode fields for pincode configs) from mixed-payload requests. If the request contains no authorized data left to process, it aborts with a 403 Forbidden response. --- ### Technical Flow #### 1. Resolve the selected-offer automated-pricing mode `Admin\EMICalculation::getEMIs($offer)` receives the exact offer being previewed or generated. It calls `RiskController::getAutomatedRiskPricingEligibility($applicationId, $offer, $baseRoi)`, which evaluates automated PF and automated ROI independently, then uses raw `$offer->tenure` only if both are eligible. The domain EMI calculator receives the resulting automated-PF decision before it calculates PF. This shared path covers the admin EMI preview, manual plan changes, approval-time EMI generation, and Table of Offers regeneration. It prevents a stale application offer or tenure from controlling pricing. #### 2. Calculate PF `App\EMICalculation::setProcessingFee()` and `updateProcessingFeeOnRemainingAmount()` both pass the automated-PF decision to `RiskController`. * If automated PF is the final selected automated charge, the existing automated bureau PF surcharge can be added when all existing eligibility checks pass. * The system evaluates both the bureau PF surcharge and the Zero-ROI Pincode PF penalty, applying the maximum of the two percentages. * The automated PF methods return the unchanged base PF and zero automated surcharge components only when PF is not the final selected automated charge. * In both cases, `getRiskBasedPFCalculation()` still runs afterward, so manual PF remains independent of the automated tenure gate. * The generated EMI result includes `pf_surcharge_percent`, `pf_surcharge_amount`, and `pf_surcharge_gst` for persistence. #### 3. Calculate ROI `Admin\EMICalculation::getEMIs()` evaluates `getAutomatedROI()` before the tenure decision. The method evaluates the merchant gate, bureau score and date, risky pincode, category configuration, and priority math. Automated ROI is selected whenever it is the only eligible automated charge, or when both charges are eligible and the offer tenure is longer than 12 months. If automated ROI is selected, the final ROI is: ```text merchant_offers.interest_rate + applied_roi_percentage ``` After automated calculation, the existing manual ROI logic is evaluated. A manual ROI overrides the automated final rate and causes automated ROI metadata to be persisted as zero for that EMI plan. #### 4. Persist the final EMI plan The generated plan carries these automated ROI fields: * `bureau_roi_percentage` * `pincode_roi_percentage` * `applied_roi_percentage` They are persisted on both `emi_calculation` and `emi_calculation_lock`. The three PF surcharge fields are also carried through the generated EMI payload and persisted with the plan. `TableOfOffersTrait::regenerateEMIAfterDecision()` forwards all six final risk fields from the selected-offer calculation to `Admin\EMICalculation::generateEMI()`: `pf_surcharge_percent`, `pf_surcharge_amount`, `pf_surcharge_gst`, `bureau_roi_percentage`, `pincode_roi_percentage`, and `applied_roi_percentage`. This makes Table of Offers use the same persistence contract as the normal EMI-generation flow, including when an EMI row is created for the first time. Whenever the final automated selection changes from PF to ROI, the PF surcharge fields are written as zero so a previous automated PF surcharge cannot remain on the plan. Whenever it changes from ROI to PF, all three automated ROI fields are written as zero so a previous automated ROI loading cannot remain on the plan. These resets affect automated metadata only and never remove manual PF or ROI data. #### 5. Keep Table of Offers calculations read-only until final selection Every Table of Offers calculation sends `risk_pricing_preview = true`. This includes regular/minimum-tenure candidates, residual-EMI candidates, bureau-surrogate candidates, Power Saving, Banking ABB, and the final selected-offer preview used to build the final EMI request. In preview mode, `App\EMICalculation` still returns the exact same total PF and surcharge components, but passes a no-persist flag to `RiskController::getProcessingFee()` and `getProcessingFeesForNetLoanAmount()`. The PF helper methods therefore do not call `updateSurcharge()` while an offer is being evaluated. Candidate calculations cannot overwrite `emi_calculation` PF components, change an EMI lock snapshot, or create the `Risk Based Pricing Surcharge Added` comment. The following `generateEMI()` call is the sole persistence point for the selected TOF offer. After it saves the final EMI row, `RiskController::ensureAutomatedPFSurchargeComment()` unconditionally adds the PF surcharge comment every time the final persisted automated PF surcharge is positive. This creates a detailed audit trail of every EMI regeneration without creating a comment for a rejected candidate. Non-TOF calculations retain the previous behavior because automated PF persistence defaults to enabled unless preview mode is explicitly requested. #### 6. Persist candidate pricing for the Table of Offers webhook The Table of Offers webhook represents every eligible candidate, not only the selected final offer. It therefore must use the pricing snapshot calculated for that candidate and must not use the active `emi_calculation` row, which belongs only to the selected offer. * Regular/minimum-tenure candidates already store their calculated total `interest_rate` and `processing_fee` in `loan_decision_table`. * Bureau-surrogate candidates now store their bureau-limit-specific total `interest_rate` and `processing_fee` in `bureau_surrogate_limit_interest_rate` and `bureau_surrogate_limit_processing_fee`. These are separate from the regular candidate fields because percentage PF can differ when the bureau-limit amount differs from the regular amount. * Power Saving and Banking ABB candidates now store their calculated total `interest_rate` and `processing_fee` alongside their EMI and `calculated_approved_amount` in `loan_decision_approved_amount`. * The webhook reads these persisted total values. It falls back to the base merchant-offer values only for historical decision rows created before these fields existed. For every candidate, the stored total ROI and PF are from the same EMI calculation that produced the displayed EMI and eligible amount. The automated tenure rule, manual-pricing behavior, and ROI priority math are therefore identical between the candidate calculation and the webhook display. #### 7. Synchronize final offer and tenure The standard manual application edit flow already updates application tenure from the chosen offer. The Table of Offers final-offer flow now also updates `consumer_application.offer_id` and `consumer_application.tenure` from the same final `MerchantOffers` record after EMI regeneration. This keeps the application record aligned with the final plan. --- ### Approval Deviations and Comments `ConsumerApplicationController::handleRiskBasedPricingDeviations()` treats an EMI as having automated ROI only when `applied_roi_percentage > 0`. * **Deviation Triggers**: Deviations are logged automatically during the BRE auto-approval flow (via the `AutoDecision` cron job, using the `autoDecisionQueue` flag). They are also correctly logged when a user manually changes the application status to "waiting for decision" via the UI, which triggers a synchronous BRE auto-approval and redirects to the "approved" flow with the `is_auto_approved_via_ui` flag. * A positive bureau component creates a Type 1 deviation with the credit score. * A positive pincode component creates a Type 5 deviation with the risky pincode and address type. * If both components are positive, deviations are created for both sources, including when their loading values are equal. * The application-level comment identifies the priority-math source. The existing deterministic order selects the pincode wording first when both values equal the applied loading. * Manual ROI and short-tenure plans have `applied_roi_percentage = 0`, so they do not create automated-ROI deviations or comments. * Automated PF surcharge comments are now created only after final EMI persistence; TOF candidate evaluation does not create them. --- ### Data Model and Migrations Automated ROI data is intentionally separate from manual ROI history. | Table | Relevant fields | Purpose | | --- | --- | --- | | `merchant_offers` | `tenure`, `interest_rate`, PF configuration | Selected offer and raw tenure used for pricing. | | `risk_based_configuration` | PF/ROI bureau-band values and active flags | Automated bureau pricing configuration. | | `merchant_category` | `risky_pincode_roi`, `is_risky_pincode_active` | Automated risky-pincode ROI configuration. | | `risk_based_pricing_calculation` | manual PF adjustment | Manual PF; unaffected by tenure gating. | | `risk_based_interest_rate_changes` | manual ROI override | Manual ROI; unaffected by tenure gating. | | `loan_decision_table` | regular candidate totals plus bureau-surrogate amount/EMI/ROI/PF fields | Regular and bureau-surrogate TOF decision snapshots. | | `loan_decision_approved_amount` | custom-flow candidate amount, EMI, total ROI, total PF | Power Saving and Banking ABB decision snapshots. | | `emi_calculation` | PF surcharge fields and the three automated ROI fields | Final active EMI pricing result. | | `emi_calculation_lock` | PF surcharge fields and the three automated ROI fields | Historical/frozen EMI snapshot. | The existing ROI migrations define all three automated ROI fields per table: * `2026_07_23_120300_add_risk_based_roi_percentages_to_emi_calculation_table.php` * `2026_07_23_120400_add_risk_based_roi_percentages_to_emi_calculation_lock_table.php` Each migration adds `bureau_roi_percentage`, `pincode_roi_percentage`, and `applied_roi_percentage` as `double(4, 2)` columns with default `0`. --- ### Main Code Impact | File | Change | | --- | --- | | `config/constants.php` & `config/permission_tooltips.json` | Defines new RBAC permissions (`Bureau Configuration Update`, `Risky Pincode Update`, `Activate Risk Based Pricing`) and tooltip strings. | | `app/Http/Controllers/Admin/RiskController.php` | Enforces RBAC permissions on the `updateRiskBasedPricing` endpoint. Resolves PF and ROI eligibility independently, applies the raw-offer tenure choice only when both are eligible, resolves automated ROI priority math, supports read-only PF calculation, and creates the PF surcharge comment only for the final persisted result. | | `app/RiskBasedInterestRate.php` | Extends `getInterestRate` to optionally accept an `offer_id` parameter, ensuring manual ROI fetches can be scoped to specific offers. | | `app/EMICalculation.php` | Applies the automated-PF decision in normal and PF-on-remaining-amount calculations; returns surcharge components with the EMI result; supports suppressing PF surcharge persistence for previews. | | `app/Http/Controllers/Admin/EMICalculation.php` | Applies selected-offer tenure gating to automated ROI; persists PF/ROI components; clears stale automated data; synchronizes a changed application offer's tenure; creates the final PF surcharge comment after EMI persistence; scopes all manual ROI persistence queries by `offer_id`; safely restores automated ROI when a manual override is removed and prevents automated calculations from being logged as manual overrides. | | `app/Http/Controllers/Admin/ConsumerApplicationController.php` | Carries pricing components through internal generation and gates automated-ROI deviations by `applied_roi_percentage`. UI fetches for manual ROI are scoped strictly to the application's active `offer_id`. | | `app/Http/Controllers/Grade/TableOfOffersTrait.php` | Marks every TOF pricing calculation as a read-only risk-pricing preview; forwards the final selected offer's PF/ROI components into `generateEMI()`; persists source-specific candidate total ROI/PF snapshots; synchronizes final TOF offer and raw tenure on the consumer application. | | `app/LoanDecisionApprovedAmount.php` | Allows Power Saving and Banking ABB candidate total ROI/PF values to be persisted. | | `app/ApplicationEMICalculation.php` and `app/ApplicationEMICalculationLock.php` | Allow mass assignment of PF surcharge fields and all automated ROI fields. | | `resources/views/admin/layouts/footer.blade.php` | Includes automated PF/ROI components when saving an admin-generated EMI plan. | | `database/migrations/2026_07_23_120300_add_risk_based_roi_percentages_to_emi_calculation_table.php` | Defines all three automated ROI fields on `emi_calculation`. | | `database/migrations/2026_07_23_120400_add_risk_based_roi_percentages_to_emi_calculation_lock_table.php` | Defines all three automated ROI fields on `emi_calculation_lock`. | | `database/migrations/2026_07_29_120000_add_risk_pricing_columns_to_loan_decision_table.php` | Defines bureau-surrogate candidate total ROI/PF snapshot fields. | | `database/migrations/2026_07_29_120100_add_risk_pricing_columns_to_loan_decision_approved_amount_table.php` | Defines Power Saving and Banking ABB candidate total ROI/PF snapshot fields. | --- ### Operational Verification Checklist 1. Confirm the merchant risk-pricing toggle is enabled for the merchant. 2. Confirm the final selected offer and its raw tenure. 3. Verify whether automated PF and automated ROI are independently eligible for the application before using tenure as a decision factor. 4. If only one automated charge is eligible, verify it applies regardless of offer tenure. 5. If both automated charges are eligible, verify a short offer applies PF and a long offer applies ROI. 6. Confirm manual PF remains visible/effective according to user action, regardless of tenure. Confirm manual ROI remains strictly tied to the offer it was created for, and does not carry over if the active offer changes. 7. Verify bureau, pincode, and applied ROI fields on the final EMI row and lock snapshot. 8. For an automated ROI case, verify the appropriate deviation(s) and comment after automated approval. 9. Run a TOF case with multiple candidates and confirm no candidate calculation changes PF surcharge fields, EMI locks, or PF surcharge comments before the final offer is saved. 10. Confirm the final selected TOF offer persists its three PF surcharge and three automated ROI fields, and creates the PF surcharge comment only when its final automated PF surcharge is positive. 11. Confirm the TOF webhook shows the calculated total ROI and PF for every regular, bureau-surrogate, Power Saving, and Banking ABB candidate rather than the base merchant-offer values. 12. Check that the UI correctly disables inputs and shows tooltips on the Merchant Edit and Risk Based Pricing pages when the user lacks the required RBAC permissions. 13. Test that sending unauthorized payload data to `/admin/merchant/risk-based-pricing/update` safely strips or rejects the payload components according to RBAC rules.