Skip to content

Loan applications and repayment schedules

Recipe60 minExpert

Before you start

You will build the contract side of a lender or a leasing company. An application is approved and becomes a contract. A Custom function works out the instalment so that the balance reaches a target at maturity, and writes one schedule row for each period. When the customer asks for a top-up, the same calculation runs again, the old schedule stays as a superseded version, and a late payment posts a penalty. Plan on a day. The calculation is the part to test carefully, with a contract whose answer you already know.

  1. Application
  2. Approval
  3. Contract
  4. Schedule solved
  5. Top-up recalculated
  6. Settlement

Build your loan rules with modules, workflows and a script step. Read and own the schedule script below.

Six modules, four workflows and one script. The script is the centre of the recipe. Everything else is records and approvals you have seen in the other recipes.

ModuleKey fieldsNotes
Loan ProductsName, Payment style, Grace months, Late fee ratePayment style is a lookup. Late fee rate is a percentage
ApplicationsCustomer name, Product, Requested amount, Term months, StatusStatus: Draft, In review, Approved, Rejected
Loan ContractsContract number, Product, Principal, Annual rate, Term months, First due date, Residual value, Schedule version, StatusStatus: Active, Settled, Closed
Repayment ScheduleContract, Version, Instalment no, Due date, Opening balance, Interest, Principal part, Payment, Closing balance, Late fee rate, StatusStatus: Scheduled, Paid, Late, Superseded
AmendmentsContract, Type, Top-up amount, New term months, StatusType: Top-up, Extension
Penalty LedgerContract, Schedule line, Amount, Reason, WaivedOne row per charge, so the trail stays

Link them with the Relation button: an application to a product, a contract to a product, a schedule row to a contract, an amendment to a contract, a penalty to a contract and to a schedule row. A relation field is named after the related module’s key field by default, so a schedule row’s contract field is loan_contracts_pr_id. You use that name in the script. Open the Fields page and read the exact system names before you copy anything. See Add a relation.

Give Loan Contracts a number with a default value script, as in An approvals hub, and turn on Table and Kanban views for Applications by Status.

How does an application become a contract?

Section titled “How does an application become a contract?”

On Applications, create a workflow that runs when A record is created, After the record is saved (POST).

  1. Add Approval task, Title people will see Approve application, Who decides? A role, Credit Committee. Stage In review.
  2. On Rejected, add Record manager: Update, Status Rejected. Tick Add the rejection reason to the record.
  3. On Approved, add Record manager: Update, Status Approved.
  4. Add Record manager: Create, Loan Contracts. Fill Product, Principal (from Requested amount) and Term months with Insert data. Set Annual rate from the product, and Status Active.
An adhoc workflow in the designer: a trigger that says it is started by a button or another workflow, then a Record manager step that updates a contract.
    An Adhoc trigger is started by a button or by another workflow.
    A Record manager step changes the record that started the workflow.

A Create is a save like any other, so workflows on Loan Contracts run for it. This is how the next workflow can start from the contract.

Create an Adhoc workflow on Loan Contracts named Calculate schedule. An adhoc workflow is started by a button or by another workflow.

  1. Add Load data: Repayment Schedule where Contract is this contract and Status is Scheduled. Name it old_lines.
  2. Add Record manager: Batch update, Repayment Schedule, over old_lines. Set Status to Superseded.
  3. Add Custom function with the script below.
  4. Add Record manager: Update, Loan Contracts (the record that started it). Set Schedule version to the old value plus 1.

Steps 1 and 2 keep the previous schedule. Rows marked Superseded stay for the auditor, and a table of Scheduled rows is the live one.

The Smart Script Value editor in the dashboard builder with the function suggestions open: loadValueByClause, loadRecordsByClause, evaluateSumFunction and others.
    Type FunctionManager. and press Ctrl + Space to list the helpers you can call. The workflow editor offers the same list.
    Done closes the editor.

The script finds the instalment by bisection. It tries a payment, simulates the balance month by month, and halves the range until the balance at maturity hits the residual value. Then it writes the rows.

var key = parseInt($cm_loan_contracts_base$.$loan_contracts_pr_id$, 10);
var principal = parseFloat($cm_loan_contracts_base$.$principal$);
var months = parseInt($cm_loan_contracts_base$.$term_months$, 10);
var r = (parseFloat($cm_loan_contracts_base$.$annual_rate$) || 0) / 1200;
var residual = parseFloat($cm_loan_contracts_base$.$residual_value$) || 0;
var version = (parseInt($cm_loan_contracts_base$.$schedule_version$, 10) || 0) + 1;
var d = '$cm_loan_contracts_base$.$first_due_date$'.split('/');
function endBalance(pmt) {
var b = principal;
for (var i = 0; i < months; i++) { b = b * (1 + r) - pmt; }
return b;
}
var lo = 0, hi = principal * (1 + r);
for (var k = 0; k < 60; k++) {
var mid = (lo + hi) / 2;
if (endBalance(mid) > residual) { lo = mid; } else { hi = mid; }
}
var pmt = (lo + hi) / 2;
var HashMap = Java.type('java.util.HashMap');
var balance = principal;
for (var n = 1; n <= months; n++) {
var interest = balance * r;
var closing = balance + interest - pmt;
var due = new Date(parseInt(d[2], 10), parseInt(d[1], 10) - 1 + (n - 1), parseInt(d[0], 10));
var row = new HashMap();
row.put('loan_contracts_pr_id', String(key));
row.put('version', String(version));
row.put('instalment_no', String(n));
row.put('due_date', due.getFullYear() + '-' + ('0' + (due.getMonth() + 1)).slice(-2) + '-' + ('0' + due.getDate()).slice(-2));
row.put('opening_balance', balance.toFixed(2));
row.put('interest', interest.toFixed(2));
row.put('principal_part', (pmt - interest).toFixed(2));
row.put('payment', pmt.toFixed(2));
row.put('closing_balance', closing.toFixed(2));
row.put('status', '1');
FunctionManager.createRecords('cm_repayment_schedule_base', row);
balance = closing;
}

Read it in four pieces. The first block turns tokens into numbers, with || 0 so an empty field means zero. The endBalance function simulates the loan. The for (var k...) loop is the goal seek: 60 halvings are far more than money needs. The last loop writes one row each, with status 1 for the first value of the Schedule statuses lookup. Insert each token with Insert data, so the module table name in the token matches yours. createRecords takes the module’s table name too. The Modules page shows it on each card, and you can copy it.

Dates are written as text. The master accepts yyyy-MM-dd, which is what the script produces. The first due date arrives in the day, month, year order, so the script splits it on /. Test with one contract before you trust it, and start with a due day of 1 to 28, because a day such as the 31st moves into the next month in months that are shorter. See Custom function and Write a custom function step.

Each createRecords call is a normal save. Workflows on Repayment Schedule run for every row. Keep that module free of workflows that you do not want 36 times.

Add a Smart view button Calculate schedule to the Loan Contracts record page that runs this workflow, and a related tab that lists the Scheduled rows.

How does a top-up recalculate the schedule?

Section titled “How does a top-up recalculate the schedule?”

On Amendments, create a workflow that runs when A record is created, with an approval first if your policy needs one.

  1. After the approval’s Approved path, add Record manager: Update, Loan Contracts, with the amendment’s Contract as the Record ID. Set Principal to parseFloat($cm_loan_contracts_base$.$principal$) + parseFloat($cm_amendments_base$.$topup_amount$). The key is needed, because the amendment is a different module.
  2. Add Custom function:
FunctionManager.runAdhocWorkflow('cm_loan_contracts_base', '$cm_amendments_base$.$loan_contracts_pr_id$', 'WORKFLOW ID');

Replace WORKFLOW ID with the number of Calculate schedule, which you can read from its address in the Designer. The call runs the adhoc workflow on that contract straight away and returns a result object. Check the returned result in your script before continuing. Because the top-up changed the principal first, the new schedule uses the new amount, and the old rows are already marked Superseded.

For a partial disbursement, add a Disbursed amount field and use it in place of Principal in the script. The recalculation is the same.

Late payment. Create a workflow on Repayment Schedule, Adhoc, Also run on a schedule, Every day at 09:00.

  1. Load data: Repayment Schedule where Status is Scheduled and Due date is before today. Use a Smart Script that returns today’s date for the date rule. Name it late.
  2. Loop, Each record in a list, late.
  3. Inside, Record manager: Create, Penalty Ledger. Amount is parseFloat($late$.$payment$) * parseFloat($late$.$late_fee_rate$) / 100.
  4. Inside, Record manager: Update the schedule row, Status Late.

The row’s Late fee rate is a field the script copies from the product when it creates rows. Add a row.put for it.

Early settlement. An Adhoc workflow on Loan Contracts: load the rows that are still Scheduled, then a Record manager Batch update sets them to Superseded, and the contract’s Status moves to Settled. The payoff is the sum of their Principal part, and FunctionManager.evaluateSumFunction adds a column across rows that match a clause. Read the system names on the Fields page when you write the clause. Add a Smart document step if the customer needs a settlement letter.

Credit staff create and edit Applications. A credit committee role gets the approval tasks. Operations edit Loan Contracts and read Repayment Schedule. A finance profile may edit Penalty Ledger to waive a charge. Keep Repayment Schedule read-only on the form for everyone, so the script owns it.

  • Sharia-compliant financing. Financing through Tawarruq works by integrating with the Tawarruq engine. The application, approval and contract parts of this recipe stay the same.
  • Different payment styles. Add a Payment style lookup value and a Decision in the schedule workflow, one Custom function per style.
  • Post to accounting. A REST API or Integration step sends each approved contract on. See Talking to any system.
  • Customer history. A Smart view shows the contract’s schedule versions on a tab.

The Solutions page follows one lending contract from product setup to settlement: Lending and financing.

Check yourself

Youssef's Custom function should create 36 schedule rows. How does he confirm they exist before the workflow goes on?
Show the answer

Add Load data for the new rows and a Decision on has no records, with a Terminate on the empty path. The script writes records. Loading them afterwards and testing the list with has no records proves the rows exist, and a Terminate set to Failed stops the workflow when they do not.

Why does the schedule script wrap each token in parseFloat or parseInt?
Show the answer

Tokens for Text fields arrive as quoted strings, so arithmetic needs numbers. Smart Script replaces a token with the field's value, and text-like values arrive as strings. Converting with parseFloat or parseInt makes the arithmetic reliable.

What's next

Last checked