Repository navigation
FINERACT-1322: Add semi-monthly repayment frequency - #6546
alberto-art3ch wants to merge 1 commit into
Conversation
2e93153 to
47825ce
Compare
53c84e0 to
a81e30a
Compare
|
@alberto-art3ch Please review below my concerns: 1. The schedule is wrong end to end, and CI shows itAll 8 schedule-generation tests in the PR's own
2. The configuration doesn't match the spec
The only pairs you can set are 1&15, 2&17 … 13&28, 14&29, and 15&Last. Many pairs the spec allows can't be configured at all: 1 & Last Day, 1 & 20, 10 & 20, 5 & 25, 20 & Last Day, or any first day above 15. The spec's own examples (1&15, 15&Last, 10&25) happen to be reachable. This is a design choice the author needs to justify or change, not a bug. 3. What matches the spec
4. Smaller gaps against the spec
Suggested asks for the author
I reviewed the PR against its head commit in a temporary worktree; your current branch wasn't touched. I can post this as a PR comment if you want. |
a81e30a to
235ce7d
Compare
@adamsaghy Thanks for the thorough review — all points confirmed and addressed:
Example 3 (term in months with semi-monthly installments) is left as is: loanTermFrequencyType must equal repaymentFrequencyType for every frequency, so it's entered as 12 semi-monthly periods. This is documented. |
2bd787c to
ce9e3eb
Compare
14df895 to
9d41f42
Compare
|
@alberto-art3ch Please kindly review my concerns below: 1. Cumulative loans with daily interest get the wrong EMI (high)The PR's docs list cumulative loans with daily interest as supported, but the EMI for them is wrong.
2.
|
f76a588 to
cb152cc
Compare
@adamsaghy Thanks for the thorough review — all points confirmed and addressed:
While checking parity I also found that the progressive EMI rate factor lacked the 1/24 "same as repayment period" case its interest twin already had. The EMI evening-out hid it (same schedule either way), but it's fixed with a test on the rate factor. I'll update the PR title to "FINERACT-1322: Add semi-monthly repayment frequency". Docs (semi-monthly-repayment-frequency.adoc) are updated too. |
galovics
left a comment
There was a problem hiding this comment.
Hi @alberto-art3ch, thanks for the PR. Big one, but well covered at every level, and SemiMonthlyScheduleDates is clean. Questions:
PeriodFrequencyTypeis a shared core enum. Has adding a loan-only value been discussed on the dev list? It forced dummycase SEMI_MONTHLY:branches in savings and shares and a special-case filter inintegerValues(). A loan-specific frequency type would avoid leaking into everyswitch.DefaultScheduledDateGenerator.getRepaymentPeriodDatethrowsUnsupportedOperationExceptionforSEMI_MONTHLY. The re-age callers are guarded, but can a charge with fee frequency 6 reach it (LoanChargeWritePlatformServiceImpl)? Pls add a test or a validation.validateSemiMonthlyRepayment/validateSemiMonthlyDueDaysinLoanApplicationValidatoris a near copy ofLoanProductDataValidator. Can we share one helper next toSemiMonthlyScheduleDatesso the 1-27 / 2-31 rules cannot drift?toRecurrencefor a second day of 29 or 30: the Javadoc admits iCalendar skips the day in short months whilenext()uses the last day, so the rest/compounding date would not match the due date in February. Map a capped second day to-1, reject "same as repayment period" for semi-monthly, or at least add a test with 30.- Why a per-loan override of the days at all? A payroll loan is a product-level concern. The fallback chain (request, loan, product) adds a lot of surface; could we ship product-only first?
@Setteron both days plussetFirstRepaymentDayOfMonth(...)after construction in every factory is easy to forget at one call site. Builder or constructor?- Housekeeping: e2e shard 12 is red, the 0260 changeset will collide if another lands first, and pls squash.
cb152cc to
de1245e
Compare
Thanks @galovics:
|
de1245e to
412bc3a
Compare
|
@galovics @adamsaghy can anyone help please review this PR? |
412bc3a to
4ed4ddd
Compare
|
@alberto-art3ch Thanks for the updates. I re-reviewed the current head ( Verified as fixed
I also checked that working-day and holiday shifts don't make later due dates drift: the next due date is generated from the unshifted date, and only "reschedule to next repayment date" moves that base, onto a real due day. Still open
Nit
|
@adamsaghy thanks for re-checking. The three open points and the nit are addressed in the new commit: 1. Setters (galovics No.6). You're right, my earlier reply only covered 2. Shared enum (galovics No.1). I raised it on dev@ (see the screenshot attached). I agree a loan-only frequency type is the cleaner shape, but 3. Per-loan override rationale (galovics No.5). Added to the Nit. Done: a product
|
80b0795 to
a96f2bb
Compare
9eccb29 to
314f945
Compare
|
@alberto-art3ch Please resolve the conflicts. |
314f945 to
edc97e1
Compare
Conflicts solved, thanks! |
|
@bharathcgowda before merge, please confirm (or update the spec page to match) three deliberate deviations from spec v4: (1) first repayment day is 1–27, not 1–28, because 28 & Last Day collide on 28 Feb in non-leap years (Rule 3); (2) the second day accepts 29 and 30, falling back to the last day in shorter months (Rule 2 allows only 2–28 or Last Day); (3) re-aging rejects the |
@adamsaghy thanks. Confirming the three deviations from spec v4 as deliberate; the code, the fineract-doc chapter and the Swagger descriptions all state them the same way:
@bharathcgowda if you update the spec page, the chapter at fineract-doc/src/docs/en/chapters/features/semi-monthly-repayment-frequency.adoc has the wording for all three. Follow-up ticket for the loan-only frequency type: FINERACT-2992. It keeps the persisted values (0–3 and 6) so it needs no migration and no API change; it retypes repaymentPeriodFrequencyType across the loan domain and removes SEMI_MONTHLY from PeriodFrequencyType together with the dummy branches in savings, deposits, shares and interest rate charts. JIRA summary of FINERACT-1322 renamed from "Bi Monthly" to "Semi-Monthly". |
|
@adamsaghy Thank you for reviewing and sharing the feedback. |
edc97e1 to
1b5f447
Compare
|
@adamsaghy, could you please continue reviewing this PR, as the shared concerns have now been addressed? |
adamsaghy
left a comment
There was a problem hiding this comment.
Reviewed against the PR description. Create/read paths match; two gaps on loan modification and one description nit, see inline.
1b5f447 to
07f41ba
Compare
Changes requested solved, Thanks! @adamsaghy |
5df2c83 to
736d73d
Compare
adamsaghy
left a comment
There was a problem hiding this comment.
I ran two lifecycle cases on a local build of this head (progressive loans, interest recalculation on). Numbers below are from those runs.
| return rateFactorByRepaymentPeriod(interestRate, BigDecimal.ONE, repaymentEvery, BigDecimal.valueOf(52), actualDaysInPeriod, | ||
| calculatedDaysInPeriod, mc); | ||
| } | ||
| if (loanProductRelatedDetail.getRepaymentPeriodFrequencyType().isSemiMonthly()) { |
There was a problem hiding this comment.
Re-aged monthly installments are charged half the interest.
This affects a progressive semi-monthly loan with interest recalculation and interest calculated "same as repayment period". Re-aging can't use SEMI_MONTHLY, so it always switches the loan to another frequency (here months). The re-aged periods are attached in attachTemporaryScheduleModelReAgedPeriodsToExistingModel with the loan's own configuration, so their interest is worked out here: 1/24 of the annual rate per period, even though each re-aged period is a month.
I ran it on this head. All three loans have 24% interest and were re-aged on 20 March 2023, before maturity, into 3 monthly installments starting 15 April:
| Loan | 2nd re-aged installment | 3rd re-aged installment |
|---|---|---|
| Semi-monthly 15/31, same as repayment | 7.03 on 703.15 (1%) | 3.53 on 353.33 (1%) |
| Monthly, same as repayment (control) | 14.26 on 712.85 (2%) | 7.20 on 359.96 (2%) |
| Semi-monthly 15/31, daily interest | 13.79 (30 days) | 7.19 (31 days) |
The EMI is computed at the halved rate too (356.85 against 367.15 on the monthly loan). The loan still amortises, but it charges half the interest. Daily interest is fine.
Could the re-aged periods be charged by their own frequency? Alternatively, could re-aging a semi-monthly loan be restricted to combinations that charge correctly, with a test for a re-age with MONTHS?
There was a problem hiding this comment.
@bharathcgowda @alberto-art3ch Please validate my findings
| if (loanApplicationTerms.getRepaymentPeriodFrequencyType().isSemiMonthly()) { | ||
| // The two due days are fixed points of the calendar month, so the next date is the first of them | ||
| // after the previous one rather than an offset added to it. | ||
| return SemiMonthlyScheduleDates.next(lastRepaymentDate, loanApplicationTerms.semiMonthlyDueDays()); |
There was a problem hiding this comment.
A one-day first period is charged a full half-month by default.
Without a pinned first repayment date, the first due date is simply the next due day after disbursement, so it can be a day away. I ran this on this head with due days 1 and 16, disbursed on 15 January 2023, 1000 at 24% over 4 installments:
| Interest | 1st installment (15 → 16 January) | Interest in it |
|---|---|---|
| Same as repayment | 256.28 | 10.00 (a full 1/24) |
| Daily | 253.90 | 0.66 |
This happens even with allowPartialPeriodInterestCalculation on. A monthly loan behaves the same if its first date is pinned one day out (20.00 for one day). The difference is that a monthly loan's default first date is a full month away, while a semi-monthly loan gets the short period without anyone asking for it. Only minimumDaysBetweenDisbursalAndFirstRepayment avoids it.
Is that intended? If not, the default first due date could skip a due day that is closer than some minimum gap, or the docs could say to set the minimum gap for semi-monthly products.
There was a problem hiding this comment.
@bharathcgowda @alberto-art3ch Please validate my findings
There was a problem hiding this comment.
When allowPartialPeriodInterestCalculation is enabled for same as repayment, interest should be calculated only for the number of days between disbursement and repayment.
A repayment made the day after disbursement is valid under this configuration, but interest should still be calculated for that one day.
However, if minimumDaysBetweenDisbursalAndFirstRepayment is configured, the system should prioritize and enforce this setting.
736d73d to
67d7e77
Compare
Confirmed, thanks for running it. With interest "same as repayment period" the rate factor only looks at the loan's own Charging a re-aged period by its own frequency needs a frequency per period in the model, and the same mechanism today prices a monthly loan re-aged into weeks at 1/12 per week (the For this PR I took your second option. Chapter updated: error table and Limitations now state that a semi-monthly loan is re-aged only with daily interest, and why. |
67d7e77 to
7134f88
Compare
7134f88 to
1a7f8a0
Compare
|
@alberto-art3ch Please review the failing IT tests and let me know once all fixed and i can review again |
1a7f8a0 to
40bb7bb
Compare

Description
Adds a semi-monthly repayment frequency (
repaymentFrequencyType = 6,SEMI_MONTHLY): a loan repaid twice every calendar month on two configured days.Fineract supports daily, weekly, every-n-weeks, monthly and yearly repayments, but not two repayments within the same month on fixed days (e.g. payroll on the 15th and the last day of the month).
Configuration
firstRepaymentDayOfMonth(1–27) andsecondRepaymentDayOfMonth(2–31, greater than the first). A second day the month lacks falls on its last day, so 31 means "last day of the month". Both days are sent together;repaymentEverymust be 1.CUMULATIVEandPROGRESSIVEschedules.Schedule and interest
API
GET /loanproducts/{id}(also with?template=true),GET /loans/templateandGET /loans/{id}return both days.error.msg.loan.application.first.repayment.date.not.a.semi.monthly.due.day.Limitations
DAYS,WEEKS,MONTHSandYEARS;SEMI_MONTHLYis rejected with 400.FINERACT-1322
Checklist
Please make sure these boxes are checked before submitting your pull request - thanks!
Your assigned reviewer(s) will follow our guidelines for code reviews.