From b8fbd5db85599d03ef80913ce427de25da840304 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Fri, 27 Feb 2026 13:34:36 +0200 Subject: [PATCH] FINERACT-2455: Add projected amortization schedule --- .../src/docs/en/chapters/features/index.adoc | 1 + ...working-capital-amortization-schedule.adoc | 201 ++ ...ingCapitalAmortizationScheduleStepDef.java | 203 ++ ...WorkingCapitalAmortizationSchedule.feature | 216 ++ ...InternalWorkingCapitalLoanApiResource.java | 80 + ...alLoanAmortizationScheduleApiResource.java | 68 + ...ojectedAmortizationScheduleCalculator.java | 57 + ...ojectedAmortizationScheduleCalculator.java | 73 + .../ProjectedAmortizationScheduleModel.java | 430 +++ .../calc/ProjectedPayment.java | 80 + .../workingcapitalloan/calc/TvmFunctions.java | 157 ++ .../ProjectedAmortizationScheduleData.java | 43 + ...edAmortizationScheduleGenerateRequest.java | 39 + ...jectedAmortizationSchedulePaymentData.java | 48 + .../ProjectedAmortizationLoanModel.java | 57 + ...AmortizationScheduleNotFoundException.java | 29 + .../ProjectedAmortizationScheduleMapper.java | 77 + ...jectedAmortizationLoanModelRepository.java | 31 + ...mortizationScheduleModelParserService.java | 33 + ...ionScheduleModelParserServiceGsonImpl.java | 87 + ...AmortizationScheduleRepositoryWrapper.java | 34 + ...tizationScheduleRepositoryWrapperImpl.java | 66 + ...alLoanAmortizationScheduleReadService.java | 26 + ...anAmortizationScheduleReadServiceImpl.java | 58 + ...lLoanAmortizationScheduleWriteService.java | 26 + ...nAmortizationScheduleWriteServiceImpl.java | 65 + .../module-changelog-master.xml | 1 + .../parts/0009_wc_loan_amortization_model.xml | 81 + ...tedAmortizationScheduleCalculatorTest.java | 2316 +++++++++++++++++ 29 files changed, 4683 insertions(+) create mode 100644 fineract-doc/src/docs/en/chapters/features/working-capital-amortization-schedule.adoc create mode 100644 fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java create mode 100644 fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanAmortizationScheduleApiResource.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedPayment.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleData.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleGenerateRequest.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/ProjectedAmortizationLoanModel.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/ProjectedAmortizationScheduleNotFoundException.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/ProjectedAmortizationLoanModelRepository.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserServiceGsonImpl.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapper.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadServiceImpl.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java create mode 100644 fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java create mode 100644 fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0009_wc_loan_amortization_model.xml create mode 100644 fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java diff --git a/fineract-doc/src/docs/en/chapters/features/index.adoc b/fineract-doc/src/docs/en/chapters/features/index.adoc index 5c386f1e1e7..c7a212145d5 100644 --- a/fineract-doc/src/docs/en/chapters/features/index.adoc +++ b/fineract-doc/src/docs/en/chapters/features/index.adoc @@ -16,3 +16,4 @@ include::re-amortization.adoc[leveloffset=+1] include::re-ageing.adoc[leveloffset=+1] include::delayed-schedule-captures.adoc[leveloffset=+1] include::loan-origination-details.adoc[leveloffset=+1] +include::working-capital-amortization-schedule.adoc[leveloffset=+1] diff --git a/fineract-doc/src/docs/en/chapters/features/working-capital-amortization-schedule.adoc b/fineract-doc/src/docs/en/chapters/features/working-capital-amortization-schedule.adoc new file mode 100644 index 00000000000..a0d7ab5ff63 --- /dev/null +++ b/fineract-doc/src/docs/en/chapters/features/working-capital-amortization-schedule.adoc @@ -0,0 +1,201 @@ += Working Capital Loan — Projected Amortization Schedule + +== Overview + +Dynamically updated amortization schedule for Working Capital (WC) Loans — zero-interest, discount/fee-based products with flexible, sales-based repayments. Uses EIR methodology for income recognition, deferral, and NPV calculation. + +Each payment period is exactly *one day*: period `N` date = `expectedDisbursementDate + N days`. + +== Lifecycle + +[cols="1,3",options="header"] +|=== +| Stage | Behavior + +| *Create* | `generateModel()` — initial schedule from application parameters. +| *Disburse* | `addDisbursement()` — recalculates with actual disbursement amounts/date, preserves applied payments. +| *Repayment* | `applyPayment()` — records payment, full schedule rebuild. +|=== + +== Input Parameters + +[cols="2,1,4",options="header"] +|=== +| Parameter | Type | Description + +| `originationFeeAmount` | Money | Upfront discount/fee. Amortized over lifecycle. +| `netDisbursementAmount` | Money | Principal disbursed. Must be positive. +| `totalPaymentValue` | Money | Merchant's TPV. +| `periodPaymentRate` | BigDecimal | Rate applied to TPV (e.g., 0.18 = 18%). +| `npvDayCount` | Integer | Day-count convention (e.g., 360). Must be positive. +| `expectedDisbursementDate` | Date | Loan start date. +|=== + +== Formulas + +---- +expectedPaymentAmount = (TPV × periodPaymentRate) / npvDayCount +loanTerm = roundUp((netDisbursement + originationFee) / expectedPayment) +EIR = RATE(loanTerm, -expectedPayment, netDisbursement) // Newton-Raphson, tol=1E-10, max 100 iter +paymentsLeft = max(0, paymentNo - appliedPaymentCount) +discountFactor = 1 / (1 + EIR) ^ paymentsLeft // model wrapper: returns 1.0 if ≤ 0 +npvSource = actualPayment (if applied) or forecastPayment (if not) +npvValue = max(0, npvSource × discountFactor) // row 0: -netDisbursementAmount (unclamped) +---- + +== Schedule Fields + +[cols="2,4",options="header"] +|=== +| Field | Formula / Description + +| `paymentNo` | 1-based. Row 0 = disbursement. +| `paymentDate` | `expectedDisbursementDate + paymentNo` days. +| `count` | `loanTerm + appliedPaymentCount - paymentNo`. Can be negative for tail periods. +| `paymentsLeft` | `max(0, paymentNo - appliedPaymentCount)`. Zero for row 0. +| `expectedPaymentAmount` | Constant. Row 0: `-netDisbursementAmount`. Tail: null. +| `forecastPaymentAmount` | Expected payment reduced by excess (backward from last period), clamped to zero. Row 0: null. +| `discountFactor` | `1/(1+EIR)^paymentsLeft`. Row 0 and paid periods: 1.0. +| `npvValue` | `max(0, npvSource × DF)`. Row 0: `-netDisbursementAmount`. +| `balance` | `balance[i-1]×(1+EIR) - expectedPayment`. Row 0: `+netDisbursementAmount`. Tail: null. +| `expectedAmortizationAmount` | `min(balance[i] + expectedPayment - balance[i-1], originationFee)`. Row 0 / tail: null. +| `netAmortizationAmount` | Paid: `min(totalNetAmort - cumulativeActualAmort_before, originationFee)` where `cumulativeActualAmort_before` excludes the current period. Unpaid: 0. Row 0: null. +| `actualPaymentAmount` | Actual cash paid. Null if no payment. +| `actualAmortizationAmount` | Cursor-based: `actualPayment/expectedPayment` periods of expected amortization consumed. Null if no payment. +| `incomeModification` | Paid: `actualAmort - expectedAmort`. Unpaid: `-expectedAmort`. Row 0 / tail: null. +| `deferredBalance` | `originationFee - cumulativeActualAmort`. Row 0: `originationFee`. Tail: null. +|=== + +=== Disbursement Row (paymentNo = 0) + +[cols="1,2"] +|=== +| `count` | `loanTerm + appliedPaymentCount` +| `paymentsLeft` | 0 +| `expectedPaymentAmount` | `-netDisbursementAmount` +| `forecastPaymentAmount` | null +| `discountFactor` | 1.0 +| `npvValue` | `-netDisbursementAmount` +| `balance` | `+netDisbursementAmount` +| `deferredBalance` | `originationFeeAmount` +| all other nullable fields | null +|=== + +=== Tail Periods + +Appended when shortfall remains after loanTerm. Each forecasts `min(remainingShortfall, expectedPayment)`. Structural fields (`paymentNo`, `paymentDate`, `count`, `paymentsLeft`) are always set. Among nullable fields, only `forecastPaymentAmount`, `discountFactor`, `npvValue`, `netAmortizationAmount` (zero) are populated; rest are null. Trailing rows with zero forecast are trimmed. + +== Calculation Algorithm + +. *Balances & expected amortizations*: `balance[i] = balance[i-1]×(1+EIR) - expectedPayment`. Expected amort capped at `originationFee`. +. *Aggregate payments by date* (same-date payments summed). Payments must occupy consecutive day slots starting from day 1. +. *Shortfall/excess analysis*: compare each applied payment to expected. +. *Cursor-based actual amortization*: cursor advances by `actualPayment/expectedPayment` periods; interpolates partial periods. +. *Excess distribution*: reduces forecast payments backward from last period. +. *Tail periods*: appended for remaining shortfall. +. *Total net amortization*: `-netDisbursement + Σ(npvSource[i] × DF[i]) + tailNpv`. +. *Assemble rows*, trim trailing zero-forecast. + +== API Endpoints + +=== GET /v1/working-capital-loans/{loanId}/amortization-schedule + +Example Request: + + GET /fineract-provider/api/v1/working-capital-loans/1/amortization-schedule + +Example Response (first 3 payments shown): + +[source,json] +---- +{ + "originationFeeAmount": 1000.00, + "netDisbursementAmount": 9000.00, + "totalPaymentValue": 100000.00, + "periodPaymentRate": 0.18, + "npvDayCount": 360, + "expectedDisbursementDate": "2019-01-01", + "expectedPaymentAmount": 50.00, + "loanTerm": 200, + "effectiveInterestRate": 0.0010678144878363462, + "payments": [ + { + "paymentNo": 0, + "paymentDate": "2019-01-01", + "count": 200, + "paymentsLeft": 0, + "expectedPaymentAmount": -9000.00, + "forecastPaymentAmount": null, + "discountFactor": 1, + "npvValue": -9000.00, + "balance": 9000.00, + "expectedAmortizationAmount": null, + "netAmortizationAmount": null, + "actualPaymentAmount": null, + "actualAmortizationAmount": null, + "incomeModification": null, + "deferredBalance": 1000.00 + }, + { + "paymentNo": 1, + "paymentDate": "2019-01-02", + "count": 199, + "paymentsLeft": 1, + "expectedPaymentAmount": 50.00, + "forecastPaymentAmount": 50.00, + "discountFactor": 0.998933324523691, + "npvValue": 49.95, + "balance": 8959.61, + "expectedAmortizationAmount": 9.61, + "netAmortizationAmount": 0.00, + "actualPaymentAmount": null, + "actualAmortizationAmount": null, + "incomeModification": -9.61, + "deferredBalance": 1000.00 + }, + { + "paymentNo": 2, + "paymentDate": "2019-01-03", + "count": 198, + "paymentsLeft": 2, + "expectedPaymentAmount": 50.00, + "forecastPaymentAmount": 50.00, + "discountFactor": 0.9978677868439537, + "npvValue": 49.89, + "balance": 8919.18, + "expectedAmortizationAmount": 9.57, + "netAmortizationAmount": 0.00, + "actualPaymentAmount": null, + "actualAmortizationAmount": null, + "incomeModification": -9.57, + "deferredBalance": 1000.00 + } + ] +} +---- + +== Database + +.Table: `m_wc_loan_amortization_model` +[cols="2,1,3",options="header"] +|=== +| Column | Type | Description + +| `id` | BIGINT | PK, auto-increment. +| `version` | INT | Optimistic lock. +| `loan_id` | BIGINT | FK → `m_wc_loan`. Unique. +| `json_model` | LONGTEXT (MySQL/MariaDB) / TEXT (PostgreSQL) | Serialized model (Gson). `@JsonExclude` on `MathContext`, `MonetaryCurrency`; restored via `forDeserialization()`. +| `business_date` | DATE | Last generated/updated. +| `last_modified_on_utc` | DATETIME(6) / TIMESTAMP WITH TIME ZONE | Audit. MySQL / PostgreSQL. +| `json_model_version` | VARCHAR(10) | Currently `"1"`. +|=== + +=== Rebuild Flow + +Every state change triggers full rebuild: aggregate payments → build payment list (1 to loanTerm, actual or null) → balances → payment analysis → cursor amortizations → excess distribution → tail → net amortization → assemble rows → trim. + +== TVM Functions + +*RATE*: Newton-Raphson solving `pv×(1+r)^n + pmt×((1+r)^n−1)/r = 0`. Initial guess: `2×(pmt×n+pv)/(pv×n)`, fallback 0.01. Tolerance 1E-10, max 100 iterations. + +*Discount Factor*: `1/(1+eir)^days`. Returns 1.0 when days=0. Throws `IllegalArgumentException` for negative days. The model wraps this via `safeDiscountFactor()` which additionally returns 1.0 when the computed result is ≤ 0. diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java new file mode 100644 index 00000000000..b920f0e0e71 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalAmortizationScheduleStepDef.java @@ -0,0 +1,203 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.test.stepdef.loan; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.cucumber.datatable.DataTable; +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.client.feign.FineractFeignClient; +import org.apache.fineract.client.feign.services.WorkingCapitalLoansApi; +import org.apache.fineract.client.models.PostWorkingCapitalLoansResponse; +import org.apache.fineract.client.models.ProjectedAmortizationScheduleData; +import org.apache.fineract.client.models.ProjectedAmortizationScheduleGenerateRequest; +import org.apache.fineract.client.models.ProjectedAmortizationSchedulePaymentData; +import org.apache.fineract.test.stepdef.AbstractStepDef; +import org.apache.fineract.test.support.TestContext; +import org.apache.fineract.test.support.TestContextKey; +import org.assertj.core.api.SoftAssertions; + +@Slf4j +@RequiredArgsConstructor +public class WorkingCapitalAmortizationScheduleStepDef extends AbstractStepDef { + + private static final String WC_AMORT_SCHEDULE_KEY = "WC_AMORT_SCHEDULE_RESPONSE"; + + private final FineractFeignClient fineractFeignClient; + + @When("Admin generates a projected amortization schedule with originationFeeAmount {double}, netDisbursementAmount {double}, totalPaymentValue {double}, periodPaymentRate {double}, npvDayCount {int}, expectedDisbursementDate {string}") + public void generateAmortizationSchedule(final double originationFeeAmount, final double netDisbursementAmount, + final double totalPaymentValue, final double periodPaymentRate, final int npvDayCount, final String expectedDisbursementDate) { + final Long loanId = extractLoanId(); + final WorkingCapitalLoansApi api = fineractFeignClient.create(WorkingCapitalLoansApi.class); + + final ProjectedAmortizationScheduleGenerateRequest request = new ProjectedAmortizationScheduleGenerateRequest(); + request.setOriginationFeeAmount(BigDecimal.valueOf(originationFeeAmount)); + request.setNetDisbursementAmount(BigDecimal.valueOf(netDisbursementAmount)); + request.setTotalPaymentValue(BigDecimal.valueOf(totalPaymentValue)); + request.setPeriodPaymentRate(BigDecimal.valueOf(periodPaymentRate)); + request.setNpvDayCount(npvDayCount); + request.setExpectedDisbursementDate(LocalDate.parse(expectedDisbursementDate)); + + api.generateAmortizationSchedule(loanId, request); + log.info("Generated amortization schedule for loan {}", loanId); + } + + @When("Admin retrieves the projected amortization schedule") + public void retrieveAmortizationSchedule() { + final Long loanId = extractLoanId(); + final WorkingCapitalLoansApi api = fineractFeignClient.create(WorkingCapitalLoansApi.class); + + final ProjectedAmortizationScheduleData response = api.retrieveAmortizationSchedule(loanId); + log.info("Retrieved amortization schedule for loan {}: netDisbursementAmount={}", loanId, response.getNetDisbursementAmount()); + TestContext.INSTANCE.set(WC_AMORT_SCHEDULE_KEY, response); + } + + @Then("The retrieved amortization schedule has the following summary fields:") + public void verifyRetrievedSummaryFields(final DataTable dataTable) { + verifySummaryFields(dataTable); + } + + @Then("The retrieved amortization schedule has payments with the following details:") + public void verifyRetrievedPaymentDetails(final DataTable dataTable) { + verifyPaymentDetails(dataTable); + } + + private void verifySummaryFields(final DataTable dataTable) { + final ProjectedAmortizationScheduleData response = TestContext.INSTANCE.get(WC_AMORT_SCHEDULE_KEY); + assertThat(response).as("Amortization schedule response").isNotNull(); + + final Map expected = dataTable.asMaps().getFirst(); + final SoftAssertions assertions = new SoftAssertions(); + + assertDecimal(assertions, "originationFeeAmount", response.getOriginationFeeAmount(), expected.get("originationFeeAmount")); + assertDecimal(assertions, "netDisbursementAmount", response.getNetDisbursementAmount(), expected.get("netDisbursementAmount")); + assertDecimal(assertions, "totalPaymentValue", response.getTotalPaymentValue(), expected.get("totalPaymentValue")); + assertDecimal(assertions, "periodPaymentRate", response.getPeriodPaymentRate(), expected.get("periodPaymentRate")); + assertInt(assertions, "npvDayCount", response.getNpvDayCount(), expected.get("npvDayCount")); + assertDecimal(assertions, "expectedPaymentAmount", response.getExpectedPaymentAmount(), expected.get("expectedPaymentAmount")); + assertInt(assertions, "loanTerm", response.getLoanTerm(), expected.get("loanTerm")); + + assertions.assertAll(); + } + + private void verifyPaymentDetails(final DataTable dataTable) { + final ProjectedAmortizationScheduleData response = TestContext.INSTANCE.get(WC_AMORT_SCHEDULE_KEY); + assertThat(response).as("Amortization schedule response").isNotNull(); + + final List actualPayments = response.getPayments(); + assertThat(actualPayments).as("payments list").isNotNull(); + + final List> expectedRows = dataTable.asMaps(); + assertThat(actualPayments).as("payment count").hasSameSizeAs(expectedRows); + + final SoftAssertions assertions = new SoftAssertions(); + for (int i = 0; i < expectedRows.size(); i++) { + final Map expected = expectedRows.get(i); + final ProjectedAmortizationSchedulePaymentData actual = actualPayments.get(i); + final String p = "payment[" + i + "]."; + + assertInt(assertions, p + "paymentNo", actual.getPaymentNo(), expected.get("paymentNo")); + assertDate(assertions, p + "date", actual.getPaymentDate(), expected.get("date")); + assertLong(assertions, p + "paymentsLeft", actual.getPaymentsLeft(), expected.get("paymentsLeft")); + assertNullableDecimal(assertions, p + "expectedPaymentAmount", actual.getExpectedPaymentAmount(), + expected.get("expectedPaymentAmount")); + assertNullableDecimal(assertions, p + "forecastPaymentAmount", actual.getForecastPaymentAmount(), + expected.get("forecastPaymentAmount")); + assertOptionalDecimal(assertions, p + "discountFactor", actual.getDiscountFactor(), expected.get("discountFactor")); + assertNullableDecimal(assertions, p + "npvValue", actual.getNpvValue(), expected.get("npvValue")); + assertNullableDecimal(assertions, p + "balance", actual.getBalance(), expected.get("balance")); + assertNullableDecimal(assertions, p + "expectedAmortizationAmount", actual.getExpectedAmortizationAmount(), + expected.get("expectedAmortizationAmount")); + assertNullableDecimal(assertions, p + "netAmortizationAmount", actual.getNetAmortizationAmount(), + expected.get("netAmortizationAmount")); + assertNullableDecimal(assertions, p + "actualPaymentAmount", actual.getActualPaymentAmount(), + expected.get("actualPaymentAmount")); + assertNullableDecimal(assertions, p + "actualAmortizationAmount", actual.getActualAmortizationAmount(), + expected.get("actualAmortizationAmount")); + assertNullableDecimal(assertions, p + "incomeModification", actual.getIncomeModification(), expected.get("incomeModification")); + assertNullableDecimal(assertions, p + "deferredBalance", actual.getDeferredBalance(), expected.get("deferredBalance")); + } + + assertions.assertAll(); + } + + private static void assertDecimal(final SoftAssertions assertions, final String field, final BigDecimal actual, + final String expectedStr) { + if (expectedStr == null || expectedStr.isBlank()) { + return; + } + final BigDecimal expected = new BigDecimal(expectedStr); + assertions.assertThat(actual).as(field).isNotNull(); + if (actual != null) { + assertions.assertThat(actual.compareTo(expected)).as("%s: expected=%s actual=%s", field, expected, actual).isEqualTo(0); + } + } + + private static void assertNullableDecimal(final SoftAssertions assertions, final String field, final BigDecimal actual, + final String expectedStr) { + if (expectedStr == null || expectedStr.isBlank()) { + assertions.assertThat(actual).as(field + " should be null").isNull(); + return; + } + assertDecimal(assertions, field, actual, expectedStr); + } + + private static void assertInt(final SoftAssertions assertions, final String field, final Integer actual, final String expectedStr) { + if (expectedStr == null || expectedStr.isBlank()) { + return; + } + assertions.assertThat(actual).as(field).isEqualTo(Integer.parseInt(expectedStr)); + } + + private static void assertLong(final SoftAssertions assertions, final String field, final Long actual, final String expectedStr) { + if (expectedStr == null || expectedStr.isBlank()) { + return; + } + assertions.assertThat(actual).as(field).isEqualTo(Long.parseLong(expectedStr)); + } + + private static void assertDate(final SoftAssertions assertions, final String field, final LocalDate actual, final String expectedStr) { + if (expectedStr == null || expectedStr.isBlank()) { + return; + } + assertions.assertThat(actual).as(field).isEqualTo(LocalDate.parse(expectedStr)); + } + + private static void assertOptionalDecimal(final SoftAssertions assertions, final String field, final BigDecimal actual, + final String expectedStr) { + if (expectedStr == null || expectedStr.isBlank()) { + assertions.assertThat(actual).as(field + " should not be null").isNotNull(); + return; + } + assertDecimal(assertions, field, actual, expectedStr); + } + + private Long extractLoanId() { + final PostWorkingCapitalLoansResponse response = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); + return response.getLoanId(); + } +} diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature new file mode 100644 index 00000000000..48384646ab3 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalAmortizationSchedule.feature @@ -0,0 +1,216 @@ +Feature: WorkingCapitalAmortizationSchedule + + Scenario: Generate and retrieve a projected amortization schedule with 200 payments + When Admin sets the business date to "01 January 2026" + And Admin creates a client with random data + And Admin creates a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + When Admin generates a projected amortization schedule with originationFeeAmount 1000.0, netDisbursementAmount 9000.0, totalPaymentValue 100000.0, periodPaymentRate 0.18, npvDayCount 360, expectedDisbursementDate "2019-01-01" + And Admin retrieves the projected amortization schedule + Then The retrieved amortization schedule has the following summary fields: + | originationFeeAmount | netDisbursementAmount | totalPaymentValue | periodPaymentRate | npvDayCount | expectedPaymentAmount | loanTerm | + | 1000.00 | 9000.00 | 100000.00 | 0.18 | 360 | 50.00 | 200 | + And The retrieved amortization schedule has payments with the following details: + | paymentNo | date | paymentsLeft | expectedPaymentAmount | forecastPaymentAmount | discountFactor | npvValue | balance | expectedAmortizationAmount | netAmortizationAmount | actualPaymentAmount | actualAmortizationAmount | incomeModification | deferredBalance | + | 0 | 2019-01-01 | 0 | -9000.00 | | 1 | -9000.00 | 9000.00 | | | | | | 1000.00 | + | 1 | 2019-01-02 | 1 | 50.00 | 50.00 | | 49.95 | 8959.61 | 9.61 | 0.00 | | | -9.61 | 1000.00 | + | 2 | 2019-01-03 | 2 | 50.00 | 50.00 | | 49.89 | 8919.18 | 9.57 | 0.00 | | | -9.57 | 1000.00 | + | 3 | 2019-01-04 | 3 | 50.00 | 50.00 | | 49.84 | 8878.70 | 9.52 | 0.00 | | | -9.52 | 1000.00 | + | 4 | 2019-01-05 | 4 | 50.00 | 50.00 | | 49.79 | 8838.18 | 9.48 | 0.00 | | | -9.48 | 1000.00 | + | 5 | 2019-01-06 | 5 | 50.00 | 50.00 | | 49.73 | 8797.62 | 9.44 | 0.00 | | | -9.44 | 1000.00 | + | 6 | 2019-01-07 | 6 | 50.00 | 50.00 | | 49.68 | 8757.01 | 9.39 | 0.00 | | | -9.39 | 1000.00 | + | 7 | 2019-01-08 | 7 | 50.00 | 50.00 | | 49.63 | 8716.36 | 9.35 | 0.00 | | | -9.35 | 1000.00 | + | 8 | 2019-01-09 | 8 | 50.00 | 50.00 | | 49.57 | 8675.67 | 9.31 | 0.00 | | | -9.31 | 1000.00 | + | 9 | 2019-01-10 | 9 | 50.00 | 50.00 | | 49.52 | 8634.94 | 9.26 | 0.00 | | | -9.26 | 1000.00 | + | 10 | 2019-01-11 | 10 | 50.00 | 50.00 | | 49.47 | 8594.16 | 9.22 | 0.00 | | | -9.22 | 1000.00 | + | 11 | 2019-01-12 | 11 | 50.00 | 50.00 | | 49.42 | 8553.33 | 9.18 | 0.00 | | | -9.18 | 1000.00 | + | 12 | 2019-01-13 | 12 | 50.00 | 50.00 | | 49.36 | 8512.47 | 9.13 | 0.00 | | | -9.13 | 1000.00 | + | 13 | 2019-01-14 | 13 | 50.00 | 50.00 | | 49.31 | 8471.56 | 9.09 | 0.00 | | | -9.09 | 1000.00 | + | 14 | 2019-01-15 | 14 | 50.00 | 50.00 | | 49.26 | 8430.60 | 9.05 | 0.00 | | | -9.05 | 1000.00 | + | 15 | 2019-01-16 | 15 | 50.00 | 50.00 | | 49.21 | 8389.61 | 9.00 | 0.00 | | | -9.00 | 1000.00 | + | 16 | 2019-01-17 | 16 | 50.00 | 50.00 | | 49.15 | 8348.56 | 8.96 | 0.00 | | | -8.96 | 1000.00 | + | 17 | 2019-01-18 | 17 | 50.00 | 50.00 | | 49.10 | 8307.48 | 8.91 | 0.00 | | | -8.91 | 1000.00 | + | 18 | 2019-01-19 | 18 | 50.00 | 50.00 | | 49.05 | 8266.35 | 8.87 | 0.00 | | | -8.87 | 1000.00 | + | 19 | 2019-01-20 | 19 | 50.00 | 50.00 | | 49.00 | 8225.18 | 8.83 | 0.00 | | | -8.83 | 1000.00 | + | 20 | 2019-01-21 | 20 | 50.00 | 50.00 | | 48.94 | 8183.96 | 8.78 | 0.00 | | | -8.78 | 1000.00 | + | 21 | 2019-01-22 | 21 | 50.00 | 50.00 | | 48.89 | 8142.70 | 8.74 | 0.00 | | | -8.74 | 1000.00 | + | 22 | 2019-01-23 | 22 | 50.00 | 50.00 | | 48.84 | 8101.39 | 8.69 | 0.00 | | | -8.69 | 1000.00 | + | 23 | 2019-01-24 | 23 | 50.00 | 50.00 | | 48.79 | 8060.04 | 8.65 | 0.00 | | | -8.65 | 1000.00 | + | 24 | 2019-01-25 | 24 | 50.00 | 50.00 | | 48.74 | 8018.65 | 8.61 | 0.00 | | | -8.61 | 1000.00 | + | 25 | 2019-01-26 | 25 | 50.00 | 50.00 | | 48.68 | 7977.21 | 8.56 | 0.00 | | | -8.56 | 1000.00 | + | 26 | 2019-01-27 | 26 | 50.00 | 50.00 | | 48.63 | 7935.73 | 8.52 | 0.00 | | | -8.52 | 1000.00 | + | 27 | 2019-01-28 | 27 | 50.00 | 50.00 | | 48.58 | 7894.21 | 8.47 | 0.00 | | | -8.47 | 1000.00 | + | 28 | 2019-01-29 | 28 | 50.00 | 50.00 | | 48.53 | 7852.63 | 8.43 | 0.00 | | | -8.43 | 1000.00 | + | 29 | 2019-01-30 | 29 | 50.00 | 50.00 | | 48.48 | 7811.02 | 8.39 | 0.00 | | | -8.39 | 1000.00 | + | 30 | 2019-01-31 | 30 | 50.00 | 50.00 | | 48.42 | 7769.36 | 8.34 | 0.00 | | | -8.34 | 1000.00 | + | 31 | 2019-02-01 | 31 | 50.00 | 50.00 | | 48.37 | 7727.66 | 8.30 | 0.00 | | | -8.30 | 1000.00 | + | 32 | 2019-02-02 | 32 | 50.00 | 50.00 | | 48.32 | 7685.91 | 8.25 | 0.00 | | | -8.25 | 1000.00 | + | 33 | 2019-02-03 | 33 | 50.00 | 50.00 | | 48.27 | 7644.12 | 8.21 | 0.00 | | | -8.21 | 1000.00 | + | 34 | 2019-02-04 | 34 | 50.00 | 50.00 | | 48.22 | 7602.28 | 8.16 | 0.00 | | | -8.16 | 1000.00 | + | 35 | 2019-02-05 | 35 | 50.00 | 50.00 | | 48.17 | 7560.40 | 8.12 | 0.00 | | | -8.12 | 1000.00 | + | 36 | 2019-02-06 | 36 | 50.00 | 50.00 | | 48.12 | 7518.47 | 8.07 | 0.00 | | | -8.07 | 1000.00 | + | 37 | 2019-02-07 | 37 | 50.00 | 50.00 | | 48.06 | 7476.50 | 8.03 | 0.00 | | | -8.03 | 1000.00 | + | 38 | 2019-02-08 | 38 | 50.00 | 50.00 | | 48.01 | 7434.48 | 7.98 | 0.00 | | | -7.98 | 1000.00 | + | 39 | 2019-02-09 | 39 | 50.00 | 50.00 | | 47.96 | 7392.42 | 7.94 | 0.00 | | | -7.94 | 1000.00 | + | 40 | 2019-02-10 | 40 | 50.00 | 50.00 | | 47.91 | 7350.31 | 7.89 | 0.00 | | | -7.89 | 1000.00 | + | 41 | 2019-02-11 | 41 | 50.00 | 50.00 | | 47.86 | 7308.16 | 7.85 | 0.00 | | | -7.85 | 1000.00 | + | 42 | 2019-02-12 | 42 | 50.00 | 50.00 | | 47.81 | 7265.97 | 7.80 | 0.00 | | | -7.80 | 1000.00 | + | 43 | 2019-02-13 | 43 | 50.00 | 50.00 | | 47.76 | 7223.72 | 7.76 | 0.00 | | | -7.76 | 1000.00 | + | 44 | 2019-02-14 | 44 | 50.00 | 50.00 | | 47.71 | 7181.44 | 7.71 | 0.00 | | | -7.71 | 1000.00 | + | 45 | 2019-02-15 | 45 | 50.00 | 50.00 | | 47.66 | 7139.11 | 7.67 | 0.00 | | | -7.67 | 1000.00 | + | 46 | 2019-02-16 | 46 | 50.00 | 50.00 | | 47.60 | 7096.73 | 7.62 | 0.00 | | | -7.62 | 1000.00 | + | 47 | 2019-02-17 | 47 | 50.00 | 50.00 | | 47.55 | 7054.31 | 7.58 | 0.00 | | | -7.58 | 1000.00 | + | 48 | 2019-02-18 | 48 | 50.00 | 50.00 | | 47.50 | 7011.84 | 7.53 | 0.00 | | | -7.53 | 1000.00 | + | 49 | 2019-02-19 | 49 | 50.00 | 50.00 | | 47.45 | 6969.33 | 7.49 | 0.00 | | | -7.49 | 1000.00 | + | 50 | 2019-02-20 | 50 | 50.00 | 50.00 | | 47.40 | 6926.77 | 7.44 | 0.00 | | | -7.44 | 1000.00 | + | 51 | 2019-02-21 | 51 | 50.00 | 50.00 | | 47.35 | 6884.17 | 7.40 | 0.00 | | | -7.40 | 1000.00 | + | 52 | 2019-02-22 | 52 | 50.00 | 50.00 | | 47.30 | 6841.52 | 7.35 | 0.00 | | | -7.35 | 1000.00 | + | 53 | 2019-02-23 | 53 | 50.00 | 50.00 | | 47.25 | 6798.82 | 7.31 | 0.00 | | | -7.31 | 1000.00 | + | 54 | 2019-02-24 | 54 | 50.00 | 50.00 | | 47.20 | 6756.08 | 7.26 | 0.00 | | | -7.26 | 1000.00 | + | 55 | 2019-02-25 | 55 | 50.00 | 50.00 | | 47.15 | 6713.30 | 7.21 | 0.00 | | | -7.21 | 1000.00 | + | 56 | 2019-02-26 | 56 | 50.00 | 50.00 | | 47.10 | 6670.47 | 7.17 | 0.00 | | | -7.17 | 1000.00 | + | 57 | 2019-02-27 | 57 | 50.00 | 50.00 | | 47.05 | 6627.59 | 7.12 | 0.00 | | | -7.12 | 1000.00 | + | 58 | 2019-02-28 | 58 | 50.00 | 50.00 | | 47.00 | 6584.67 | 7.08 | 0.00 | | | -7.08 | 1000.00 | + | 59 | 2019-03-01 | 59 | 50.00 | 50.00 | | 46.95 | 6541.70 | 7.03 | 0.00 | | | -7.03 | 1000.00 | + | 60 | 2019-03-02 | 60 | 50.00 | 50.00 | | 46.90 | 6498.68 | 6.99 | 0.00 | | | -6.99 | 1000.00 | + | 61 | 2019-03-03 | 61 | 50.00 | 50.00 | | 46.85 | 6455.62 | 6.94 | 0.00 | | | -6.94 | 1000.00 | + | 62 | 2019-03-04 | 62 | 50.00 | 50.00 | | 46.80 | 6412.51 | 6.89 | 0.00 | | | -6.89 | 1000.00 | + | 63 | 2019-03-05 | 63 | 50.00 | 50.00 | | 46.75 | 6369.36 | 6.85 | 0.00 | | | -6.85 | 1000.00 | + | 64 | 2019-03-06 | 64 | 50.00 | 50.00 | | 46.70 | 6326.16 | 6.80 | 0.00 | | | -6.80 | 1000.00 | + | 65 | 2019-03-07 | 65 | 50.00 | 50.00 | | 46.65 | 6282.92 | 6.76 | 0.00 | | | -6.76 | 1000.00 | + | 66 | 2019-03-08 | 66 | 50.00 | 50.00 | | 46.60 | 6239.63 | 6.71 | 0.00 | | | -6.71 | 1000.00 | + | 67 | 2019-03-09 | 67 | 50.00 | 50.00 | | 46.55 | 6196.29 | 6.66 | 0.00 | | | -6.66 | 1000.00 | + | 68 | 2019-03-10 | 68 | 50.00 | 50.00 | | 46.50 | 6152.91 | 6.62 | 0.00 | | | -6.62 | 1000.00 | + | 69 | 2019-03-11 | 69 | 50.00 | 50.00 | | 46.45 | 6109.48 | 6.57 | 0.00 | | | -6.57 | 1000.00 | + | 70 | 2019-03-12 | 70 | 50.00 | 50.00 | | 46.40 | 6066.00 | 6.52 | 0.00 | | | -6.52 | 1000.00 | + | 71 | 2019-03-13 | 71 | 50.00 | 50.00 | | 46.35 | 6022.48 | 6.48 | 0.00 | | | -6.48 | 1000.00 | + | 72 | 2019-03-14 | 72 | 50.00 | 50.00 | | 46.30 | 5978.91 | 6.43 | 0.00 | | | -6.43 | 1000.00 | + | 73 | 2019-03-15 | 73 | 50.00 | 50.00 | | 46.25 | 5935.29 | 6.38 | 0.00 | | | -6.38 | 1000.00 | + | 74 | 2019-03-16 | 74 | 50.00 | 50.00 | | 46.20 | 5891.63 | 6.34 | 0.00 | | | -6.34 | 1000.00 | + | 75 | 2019-03-17 | 75 | 50.00 | 50.00 | | 46.15 | 5847.92 | 6.29 | 0.00 | | | -6.29 | 1000.00 | + | 76 | 2019-03-18 | 76 | 50.00 | 50.00 | | 46.10 | 5804.17 | 6.24 | 0.00 | | | -6.24 | 1000.00 | + | 77 | 2019-03-19 | 77 | 50.00 | 50.00 | | 46.06 | 5760.36 | 6.20 | 0.00 | | | -6.20 | 1000.00 | + | 78 | 2019-03-20 | 78 | 50.00 | 50.00 | | 46.01 | 5716.52 | 6.15 | 0.00 | | | -6.15 | 1000.00 | + | 79 | 2019-03-21 | 79 | 50.00 | 50.00 | | 45.96 | 5672.62 | 6.10 | 0.00 | | | -6.10 | 1000.00 | + | 80 | 2019-03-22 | 80 | 50.00 | 50.00 | | 45.91 | 5628.68 | 6.06 | 0.00 | | | -6.06 | 1000.00 | + | 81 | 2019-03-23 | 81 | 50.00 | 50.00 | | 45.86 | 5584.69 | 6.01 | 0.00 | | | -6.01 | 1000.00 | + | 82 | 2019-03-24 | 82 | 50.00 | 50.00 | | 45.81 | 5540.65 | 5.96 | 0.00 | | | -5.96 | 1000.00 | + | 83 | 2019-03-25 | 83 | 50.00 | 50.00 | | 45.76 | 5496.57 | 5.92 | 0.00 | | | -5.92 | 1000.00 | + | 84 | 2019-03-26 | 84 | 50.00 | 50.00 | | 45.71 | 5452.44 | 5.87 | 0.00 | | | -5.87 | 1000.00 | + | 85 | 2019-03-27 | 85 | 50.00 | 50.00 | | 45.66 | 5408.26 | 5.82 | 0.00 | | | -5.82 | 1000.00 | + | 86 | 2019-03-28 | 86 | 50.00 | 50.00 | | 45.62 | 5364.03 | 5.78 | 0.00 | | | -5.78 | 1000.00 | + | 87 | 2019-03-29 | 87 | 50.00 | 50.00 | | 45.57 | 5319.76 | 5.73 | 0.00 | | | -5.73 | 1000.00 | + | 88 | 2019-03-30 | 88 | 50.00 | 50.00 | | 45.52 | 5275.44 | 5.68 | 0.00 | | | -5.68 | 1000.00 | + | 89 | 2019-03-31 | 89 | 50.00 | 50.00 | | 45.47 | 5231.08 | 5.63 | 0.00 | | | -5.63 | 1000.00 | + | 90 | 2019-04-01 | 90 | 50.00 | 50.00 | | 45.42 | 5186.66 | 5.59 | 0.00 | | | -5.59 | 1000.00 | + | 91 | 2019-04-02 | 91 | 50.00 | 50.00 | | 45.37 | 5142.20 | 5.54 | 0.00 | | | -5.54 | 1000.00 | + | 92 | 2019-04-03 | 92 | 50.00 | 50.00 | | 45.32 | 5097.69 | 5.49 | 0.00 | | | -5.49 | 1000.00 | + | 93 | 2019-04-04 | 93 | 50.00 | 50.00 | | 45.28 | 5053.13 | 5.44 | 0.00 | | | -5.44 | 1000.00 | + | 94 | 2019-04-05 | 94 | 50.00 | 50.00 | | 45.23 | 5008.53 | 5.40 | 0.00 | | | -5.40 | 1000.00 | + | 95 | 2019-04-06 | 95 | 50.00 | 50.00 | | 45.18 | 4963.88 | 5.35 | 0.00 | | | -5.35 | 1000.00 | + | 96 | 2019-04-07 | 96 | 50.00 | 50.00 | | 45.13 | 4919.18 | 5.30 | 0.00 | | | -5.30 | 1000.00 | + | 97 | 2019-04-08 | 97 | 50.00 | 50.00 | | 45.08 | 4874.43 | 5.25 | 0.00 | | | -5.25 | 1000.00 | + | 98 | 2019-04-09 | 98 | 50.00 | 50.00 | | 45.03 | 4829.64 | 5.20 | 0.00 | | | -5.20 | 1000.00 | + | 99 | 2019-04-10 | 99 | 50.00 | 50.00 | | 44.99 | 4784.79 | 5.16 | 0.00 | | | -5.16 | 1000.00 | + | 100 | 2019-04-11 | 100 | 50.00 | 50.00 | | 44.94 | 4739.90 | 5.11 | 0.00 | | | -5.11 | 1000.00 | + | 101 | 2019-04-12 | 101 | 50.00 | 50.00 | | 44.89 | 4694.96 | 5.06 | 0.00 | | | -5.06 | 1000.00 | + | 102 | 2019-04-13 | 102 | 50.00 | 50.00 | | 44.84 | 4649.98 | 5.01 | 0.00 | | | -5.01 | 1000.00 | + | 103 | 2019-04-14 | 103 | 50.00 | 50.00 | | 44.80 | 4604.94 | 4.97 | 0.00 | | | -4.97 | 1000.00 | + | 104 | 2019-04-15 | 104 | 50.00 | 50.00 | | 44.75 | 4559.86 | 4.92 | 0.00 | | | -4.92 | 1000.00 | + | 105 | 2019-04-16 | 105 | 50.00 | 50.00 | | 44.70 | 4514.73 | 4.87 | 0.00 | | | -4.87 | 1000.00 | + | 106 | 2019-04-17 | 106 | 50.00 | 50.00 | | 44.65 | 4469.55 | 4.82 | 0.00 | | | -4.82 | 1000.00 | + | 107 | 2019-04-18 | 107 | 50.00 | 50.00 | | 44.60 | 4424.32 | 4.77 | 0.00 | | | -4.77 | 1000.00 | + | 108 | 2019-04-19 | 108 | 50.00 | 50.00 | | 44.56 | 4379.05 | 4.72 | 0.00 | | | -4.72 | 1000.00 | + | 109 | 2019-04-20 | 109 | 50.00 | 50.00 | | 44.51 | 4333.72 | 4.68 | 0.00 | | | -4.68 | 1000.00 | + | 110 | 2019-04-21 | 110 | 50.00 | 50.00 | | 44.46 | 4288.35 | 4.63 | 0.00 | | | -4.63 | 1000.00 | + | 111 | 2019-04-22 | 111 | 50.00 | 50.00 | | 44.41 | 4242.93 | 4.58 | 0.00 | | | -4.58 | 1000.00 | + | 112 | 2019-04-23 | 112 | 50.00 | 50.00 | | 44.37 | 4197.46 | 4.53 | 0.00 | | | -4.53 | 1000.00 | + | 113 | 2019-04-24 | 113 | 50.00 | 50.00 | | 44.32 | 4151.94 | 4.48 | 0.00 | | | -4.48 | 1000.00 | + | 114 | 2019-04-25 | 114 | 50.00 | 50.00 | | 44.27 | 4106.38 | 4.43 | 0.00 | | | -4.43 | 1000.00 | + | 115 | 2019-04-26 | 115 | 50.00 | 50.00 | | 44.22 | 4060.76 | 4.38 | 0.00 | | | -4.38 | 1000.00 | + | 116 | 2019-04-27 | 116 | 50.00 | 50.00 | | 44.18 | 4015.10 | 4.34 | 0.00 | | | -4.34 | 1000.00 | + | 117 | 2019-04-28 | 117 | 50.00 | 50.00 | | 44.13 | 3969.38 | 4.29 | 0.00 | | | -4.29 | 1000.00 | + | 118 | 2019-04-29 | 118 | 50.00 | 50.00 | | 44.08 | 3923.62 | 4.24 | 0.00 | | | -4.24 | 1000.00 | + | 119 | 2019-04-30 | 119 | 50.00 | 50.00 | | 44.04 | 3877.81 | 4.19 | 0.00 | | | -4.19 | 1000.00 | + | 120 | 2019-05-01 | 120 | 50.00 | 50.00 | | 43.99 | 3831.95 | 4.14 | 0.00 | | | -4.14 | 1000.00 | + | 121 | 2019-05-02 | 121 | 50.00 | 50.00 | | 43.94 | 3786.04 | 4.09 | 0.00 | | | -4.09 | 1000.00 | + | 122 | 2019-05-03 | 122 | 50.00 | 50.00 | | 43.90 | 3740.09 | 4.04 | 0.00 | | | -4.04 | 1000.00 | + | 123 | 2019-05-04 | 123 | 50.00 | 50.00 | | 43.85 | 3694.08 | 3.99 | 0.00 | | | -3.99 | 1000.00 | + | 124 | 2019-05-05 | 124 | 50.00 | 50.00 | | 43.80 | 3648.03 | 3.94 | 0.00 | | | -3.94 | 1000.00 | + | 125 | 2019-05-06 | 125 | 50.00 | 50.00 | | 43.76 | 3601.92 | 3.90 | 0.00 | | | -3.90 | 1000.00 | + | 126 | 2019-05-07 | 126 | 50.00 | 50.00 | | 43.71 | 3555.77 | 3.85 | 0.00 | | | -3.85 | 1000.00 | + | 127 | 2019-05-08 | 127 | 50.00 | 50.00 | | 43.66 | 3509.56 | 3.80 | 0.00 | | | -3.80 | 1000.00 | + | 128 | 2019-05-09 | 128 | 50.00 | 50.00 | | 43.62 | 3463.31 | 3.75 | 0.00 | | | -3.75 | 1000.00 | + | 129 | 2019-05-10 | 129 | 50.00 | 50.00 | | 43.57 | 3417.01 | 3.70 | 0.00 | | | -3.70 | 1000.00 | + | 130 | 2019-05-11 | 130 | 50.00 | 50.00 | | 43.52 | 3370.66 | 3.65 | 0.00 | | | -3.65 | 1000.00 | + | 131 | 2019-05-12 | 131 | 50.00 | 50.00 | | 43.48 | 3324.26 | 3.60 | 0.00 | | | -3.60 | 1000.00 | + | 132 | 2019-05-13 | 132 | 50.00 | 50.00 | | 43.43 | 3277.81 | 3.55 | 0.00 | | | -3.55 | 1000.00 | + | 133 | 2019-05-14 | 133 | 50.00 | 50.00 | | 43.38 | 3231.31 | 3.50 | 0.00 | | | -3.50 | 1000.00 | + | 134 | 2019-05-15 | 134 | 50.00 | 50.00 | | 43.34 | 3184.76 | 3.45 | 0.00 | | | -3.45 | 1000.00 | + | 135 | 2019-05-16 | 135 | 50.00 | 50.00 | | 43.29 | 3138.16 | 3.40 | 0.00 | | | -3.40 | 1000.00 | + | 136 | 2019-05-17 | 136 | 50.00 | 50.00 | | 43.24 | 3091.51 | 3.35 | 0.00 | | | -3.35 | 1000.00 | + | 137 | 2019-05-18 | 137 | 50.00 | 50.00 | | 43.20 | 3044.81 | 3.30 | 0.00 | | | -3.30 | 1000.00 | + | 138 | 2019-05-19 | 138 | 50.00 | 50.00 | | 43.15 | 2998.06 | 3.25 | 0.00 | | | -3.25 | 1000.00 | + | 139 | 2019-05-20 | 139 | 50.00 | 50.00 | | 43.11 | 2951.26 | 3.20 | 0.00 | | | -3.20 | 1000.00 | + | 140 | 2019-05-21 | 140 | 50.00 | 50.00 | | 43.06 | 2904.42 | 3.15 | 0.00 | | | -3.15 | 1000.00 | + | 141 | 2019-05-22 | 141 | 50.00 | 50.00 | | 43.01 | 2857.52 | 3.10 | 0.00 | | | -3.10 | 1000.00 | + | 142 | 2019-05-23 | 142 | 50.00 | 50.00 | | 42.97 | 2810.57 | 3.05 | 0.00 | | | -3.05 | 1000.00 | + | 143 | 2019-05-24 | 143 | 50.00 | 50.00 | | 42.92 | 2763.57 | 3.00 | 0.00 | | | -3.00 | 1000.00 | + | 144 | 2019-05-25 | 144 | 50.00 | 50.00 | | 42.88 | 2716.52 | 2.95 | 0.00 | | | -2.95 | 1000.00 | + | 145 | 2019-05-26 | 145 | 50.00 | 50.00 | | 42.83 | 2669.42 | 2.90 | 0.00 | | | -2.90 | 1000.00 | + | 146 | 2019-05-27 | 146 | 50.00 | 50.00 | | 42.79 | 2622.27 | 2.85 | 0.00 | | | -2.85 | 1000.00 | + | 147 | 2019-05-28 | 147 | 50.00 | 50.00 | | 42.74 | 2575.07 | 2.80 | 0.00 | | | -2.80 | 1000.00 | + | 148 | 2019-05-29 | 148 | 50.00 | 50.00 | | 42.69 | 2527.82 | 2.75 | 0.00 | | | -2.75 | 1000.00 | + | 149 | 2019-05-30 | 149 | 50.00 | 50.00 | | 42.65 | 2480.52 | 2.70 | 0.00 | | | -2.70 | 1000.00 | + | 150 | 2019-05-31 | 150 | 50.00 | 50.00 | | 42.60 | 2433.17 | 2.65 | 0.00 | | | -2.65 | 1000.00 | + | 151 | 2019-06-01 | 151 | 50.00 | 50.00 | | 42.56 | 2385.77 | 2.60 | 0.00 | | | -2.60 | 1000.00 | + | 152 | 2019-06-02 | 152 | 50.00 | 50.00 | | 42.51 | 2338.31 | 2.55 | 0.00 | | | -2.55 | 1000.00 | + | 153 | 2019-06-03 | 153 | 50.00 | 50.00 | | 42.47 | 2290.81 | 2.50 | 0.00 | | | -2.50 | 1000.00 | + | 154 | 2019-06-04 | 154 | 50.00 | 50.00 | | 42.42 | 2243.26 | 2.45 | 0.00 | | | -2.45 | 1000.00 | + | 155 | 2019-06-05 | 155 | 50.00 | 50.00 | | 42.38 | 2195.65 | 2.40 | 0.00 | | | -2.40 | 1000.00 | + | 156 | 2019-06-06 | 156 | 50.00 | 50.00 | | 42.33 | 2148.00 | 2.34 | 0.00 | | | -2.34 | 1000.00 | + | 157 | 2019-06-07 | 157 | 50.00 | 50.00 | | 42.29 | 2100.29 | 2.29 | 0.00 | | | -2.29 | 1000.00 | + | 158 | 2019-06-08 | 158 | 50.00 | 50.00 | | 42.24 | 2052.53 | 2.24 | 0.00 | | | -2.24 | 1000.00 | + | 159 | 2019-06-09 | 159 | 50.00 | 50.00 | | 42.20 | 2004.73 | 2.19 | 0.00 | | | -2.19 | 1000.00 | + | 160 | 2019-06-10 | 160 | 50.00 | 50.00 | | 42.15 | 1956.87 | 2.14 | 0.00 | | | -2.14 | 1000.00 | + | 161 | 2019-06-11 | 161 | 50.00 | 50.00 | | 42.11 | 1908.96 | 2.09 | 0.00 | | | -2.09 | 1000.00 | + | 162 | 2019-06-12 | 162 | 50.00 | 50.00 | | 42.06 | 1860.99 | 2.04 | 0.00 | | | -2.04 | 1000.00 | + | 163 | 2019-06-13 | 163 | 50.00 | 50.00 | | 42.02 | 1812.98 | 1.99 | 0.00 | | | -1.99 | 1000.00 | + | 164 | 2019-06-14 | 164 | 50.00 | 50.00 | | 41.97 | 1764.92 | 1.94 | 0.00 | | | -1.94 | 1000.00 | + | 165 | 2019-06-15 | 165 | 50.00 | 50.00 | | 41.93 | 1716.80 | 1.88 | 0.00 | | | -1.88 | 1000.00 | + | 166 | 2019-06-16 | 166 | 50.00 | 50.00 | | 41.88 | 1668.64 | 1.83 | 0.00 | | | -1.83 | 1000.00 | + | 167 | 2019-06-17 | 167 | 50.00 | 50.00 | | 41.84 | 1620.42 | 1.78 | 0.00 | | | -1.78 | 1000.00 | + | 168 | 2019-06-18 | 168 | 50.00 | 50.00 | | 41.79 | 1572.15 | 1.73 | 0.00 | | | -1.73 | 1000.00 | + | 169 | 2019-06-19 | 169 | 50.00 | 50.00 | | 41.75 | 1523.83 | 1.68 | 0.00 | | | -1.68 | 1000.00 | + | 170 | 2019-06-20 | 170 | 50.00 | 50.00 | | 41.70 | 1475.45 | 1.63 | 0.00 | | | -1.63 | 1000.00 | + | 171 | 2019-06-21 | 171 | 50.00 | 50.00 | | 41.66 | 1427.03 | 1.58 | 0.00 | | | -1.58 | 1000.00 | + | 172 | 2019-06-22 | 172 | 50.00 | 50.00 | | 41.61 | 1378.55 | 1.52 | 0.00 | | | -1.52 | 1000.00 | + | 173 | 2019-06-23 | 173 | 50.00 | 50.00 | | 41.57 | 1330.02 | 1.47 | 0.00 | | | -1.47 | 1000.00 | + | 174 | 2019-06-24 | 174 | 50.00 | 50.00 | | 41.53 | 1281.45 | 1.42 | 0.00 | | | -1.42 | 1000.00 | + | 175 | 2019-06-25 | 175 | 50.00 | 50.00 | | 41.48 | 1232.81 | 1.37 | 0.00 | | | -1.37 | 1000.00 | + | 176 | 2019-06-26 | 176 | 50.00 | 50.00 | | 41.44 | 1184.13 | 1.32 | 0.00 | | | -1.32 | 1000.00 | + | 177 | 2019-06-27 | 177 | 50.00 | 50.00 | | 41.39 | 1135.39 | 1.26 | 0.00 | | | -1.26 | 1000.00 | + | 178 | 2019-06-28 | 178 | 50.00 | 50.00 | | 41.35 | 1086.61 | 1.21 | 0.00 | | | -1.21 | 1000.00 | + | 179 | 2019-06-29 | 179 | 50.00 | 50.00 | | 41.31 | 1037.77 | 1.16 | 0.00 | | | -1.16 | 1000.00 | + | 180 | 2019-06-30 | 180 | 50.00 | 50.00 | | 41.26 | 988.88 | 1.11 | 0.00 | | | -1.11 | 1000.00 | + | 181 | 2019-07-01 | 181 | 50.00 | 50.00 | | 41.22 | 939.93 | 1.06 | 0.00 | | | -1.06 | 1000.00 | + | 182 | 2019-07-02 | 182 | 50.00 | 50.00 | | 41.17 | 890.93 | 1.00 | 0.00 | | | -1.00 | 1000.00 | + | 183 | 2019-07-03 | 183 | 50.00 | 50.00 | | 41.13 | 841.89 | 0.95 | 0.00 | | | -0.95 | 1000.00 | + | 184 | 2019-07-04 | 184 | 50.00 | 50.00 | | 41.09 | 792.79 | 0.90 | 0.00 | | | -0.90 | 1000.00 | + | 185 | 2019-07-05 | 185 | 50.00 | 50.00 | | 41.04 | 743.63 | 0.85 | 0.00 | | | -0.85 | 1000.00 | + | 186 | 2019-07-06 | 186 | 50.00 | 50.00 | | 41.00 | 694.43 | 0.79 | 0.00 | | | -0.79 | 1000.00 | + | 187 | 2019-07-07 | 187 | 50.00 | 50.00 | | 40.95 | 645.17 | 0.74 | 0.00 | | | -0.74 | 1000.00 | + | 188 | 2019-07-08 | 188 | 50.00 | 50.00 | | 40.91 | 595.86 | 0.69 | 0.00 | | | -0.69 | 1000.00 | + | 189 | 2019-07-09 | 189 | 50.00 | 50.00 | | 40.87 | 546.49 | 0.64 | 0.00 | | | -0.64 | 1000.00 | + | 190 | 2019-07-10 | 190 | 50.00 | 50.00 | | 40.82 | 497.08 | 0.58 | 0.00 | | | -0.58 | 1000.00 | + | 191 | 2019-07-11 | 191 | 50.00 | 50.00 | | 40.78 | 447.61 | 0.53 | 0.00 | | | -0.53 | 1000.00 | + | 192 | 2019-07-12 | 192 | 50.00 | 50.00 | | 40.74 | 398.08 | 0.48 | 0.00 | | | -0.48 | 1000.00 | + | 193 | 2019-07-13 | 193 | 50.00 | 50.00 | | 40.69 | 348.51 | 0.43 | 0.00 | | | -0.43 | 1000.00 | + | 194 | 2019-07-14 | 194 | 50.00 | 50.00 | | 40.65 | 298.88 | 0.37 | 0.00 | | | -0.37 | 1000.00 | + | 195 | 2019-07-15 | 195 | 50.00 | 50.00 | | 40.61 | 249.20 | 0.32 | 0.00 | | | -0.32 | 1000.00 | + | 196 | 2019-07-16 | 196 | 50.00 | 50.00 | | 40.56 | 199.47 | 0.27 | 0.00 | | | -0.27 | 1000.00 | + | 197 | 2019-07-17 | 197 | 50.00 | 50.00 | | 40.52 | 149.68 | 0.21 | 0.00 | | | -0.21 | 1000.00 | + | 198 | 2019-07-18 | 198 | 50.00 | 50.00 | | 40.48 | 99.84 | 0.16 | 0.00 | | | -0.16 | 1000.00 | + | 199 | 2019-07-19 | 199 | 50.00 | 50.00 | | 40.43 | 49.95 | 0.11 | 0.00 | | | -0.11 | 1000.00 | + | 200 | 2019-07-20 | 200 | 50.00 | 50.00 | | 40.39 | 0.00 | 0.05 | 0.00 | | | -0.05 | 1000.00 | diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java new file mode 100644 index 00000000000..ce308c44bf2 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/InternalWorkingCapitalLoanApiResource.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.api; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.boot.FineractProfiles; +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleGenerateRequest; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanAmortizationScheduleWriteService; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Slf4j +@RequiredArgsConstructor +@Profile(FineractProfiles.TEST) +@Component +@Path("v1/internal/working-capital-loans") +@Tag(name = "Working Capital Loans", description = "Internal WCL testing API. This API should be disabled in production!") +public class InternalWorkingCapitalLoanApiResource implements InitializingBean { + + private final WorkingCapitalLoanAmortizationScheduleWriteService writeService; + + @Override + @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") + public void afterPropertiesSet() throws Exception { + log.warn("------------------------------------------------------------"); + log.warn(" "); + log.warn("DO NOT USE THIS IN PRODUCTION!"); + log.warn("Internal client services mode is enabled"); + log.warn("DO NOT USE THIS IN PRODUCTION!"); + log.warn(" "); + log.warn("------------------------------------------------------------"); + } + + @POST + @Path("{loanId}/amortization-schedule") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Generate and save Projected Amortization Schedule (testing)", description = """ + Generates a projected amortization schedule from the provided parameters \ + and saves it for the given Working Capital Loan. + + DO NOT USE THIS IN PRODUCTION! In the real flow, the schedule will be \ + generated during loan approval/disbursement from the loan and product data.""") + @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "Working Capital Loan not found") }) + public void generateAmortizationSchedule(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + final ProjectedAmortizationScheduleGenerateRequest request) { + writeService.generateAndSaveAmortizationSchedule(loanId, request); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanAmortizationScheduleApiResource.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanAmortizationScheduleApiResource.java new file mode 100644 index 00000000000..cc91ad63952 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/api/WorkingCapitalLoanAmortizationScheduleApiResource.java @@ -0,0 +1,68 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.api; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanAmortizationScheduleReadService; +import org.springframework.stereotype.Component; + +@Path("/v1/working-capital-loans") +@Component +@Tag(name = "Working Capital Loans", description = "Working Capital Loan operations including projected amortization schedule.") +@RequiredArgsConstructor +public class WorkingCapitalLoanAmortizationScheduleApiResource { + + private final WorkingCapitalLoanAmortizationScheduleReadService readService; + + @GET + @Path("{loanId}/amortization-schedule") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Retrieve Projected Amortization Schedule", description = """ + Returns the projected amortization schedule for a Working Capital Loan. + + The schedule contains per-payment details including expected and forecast payments, \ + discount factors, NPV values, balances, expected and actual amortization amounts, \ + income modifications, and deferred balance. + + Example Request: + + working-capital-loans/1/amortization-schedule""") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = ProjectedAmortizationScheduleData.class))), + @ApiResponse(responseCode = "404", description = "Working Capital Loan or schedule not found") }) + public ProjectedAmortizationScheduleData retrieveAmortizationSchedule( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId) { + return readService.retrieveAmortizationSchedule(loanId); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java new file mode 100644 index 00000000000..c639f697608 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/DefaultProjectedAmortizationScheduleCalculator.java @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.calc; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; + +/** + * Default implementation of {@link ProjectedAmortizationScheduleCalculator}. Delegates to + * {@link ProjectedAmortizationScheduleModel} methods. + */ +@Component +public final class DefaultProjectedAmortizationScheduleCalculator implements ProjectedAmortizationScheduleCalculator { + + @Override + @NonNull + public ProjectedAmortizationScheduleModel generateModel(@NonNull final BigDecimal originationFeeAmount, + @NonNull final BigDecimal netDisbursementAmount, @NonNull final BigDecimal totalPaymentValue, + @NonNull final BigDecimal periodPaymentRate, final int npvDayCount, @NonNull final LocalDate expectedDisbursementDate, + @NonNull final MathContext mc, @NonNull final MonetaryCurrency currency) { + return ProjectedAmortizationScheduleModel.generate(originationFeeAmount, netDisbursementAmount, totalPaymentValue, + periodPaymentRate, npvDayCount, expectedDisbursementDate, mc, currency); + } + + @Override + @NonNull + public ProjectedAmortizationScheduleModel addDisbursement(@NonNull final ProjectedAmortizationScheduleModel model, + @NonNull final BigDecimal newDiscountAmount, @NonNull final BigDecimal newNetAmount, @NonNull final LocalDate newStartDate) { + return model.regenerate(newDiscountAmount, newNetAmount, newStartDate); + } + + @Override + public void applyPayment(@NonNull final ProjectedAmortizationScheduleModel model, @NonNull final LocalDate paymentDate, + @NonNull final BigDecimal paymentAmount) { + model.applyPayment(paymentDate, paymentAmount); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java new file mode 100644 index 00000000000..f7e53452aad --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculator.java @@ -0,0 +1,73 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.calc; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.springframework.lang.NonNull; + +/** + * Calculator service for Working Capital loan projected amortization schedule. Analogous to {@code EMICalculator} for + * progressive loans, but with a minimal API: generation, disbursement and payment. + */ +public interface ProjectedAmortizationScheduleCalculator { + + /** + * Creates an initial projected amortization schedule model (at loan creation). + * + * @return model with no payments applied + */ + @NonNull + ProjectedAmortizationScheduleModel generateModel(@NonNull BigDecimal originationFeeAmount, @NonNull BigDecimal netDisbursementAmount, + @NonNull BigDecimal totalPaymentValue, @NonNull BigDecimal periodPaymentRate, int npvDayCount, + @NonNull LocalDate expectedDisbursementDate, @NonNull MathContext mc, @NonNull MonetaryCurrency currency); + + /** + * Recalculates the model with updated financial parameters (at approval or disbursement). Preserves already applied + * payments on the model. + * + * @param model + * current model (may have payments) + * @param newDiscountAmount + * approved or disbursed discount + * @param newNetAmount + * approved or actual net amount + * @param newStartDate + * actual start date + * @return new model with recalculated schedule + */ + @NonNull + ProjectedAmortizationScheduleModel addDisbursement(@NonNull ProjectedAmortizationScheduleModel model, + @NonNull BigDecimal newDiscountAmount, @NonNull BigDecimal newNetAmount, @NonNull LocalDate newStartDate); + + /** + * Applies a payment to the model. The model is mutated in place; callers can read the updated payment directly from + * the model. + * + * @param model + * current model (mutated in place) + * @param paymentDate + * the date when payment was made + * @param paymentAmount + * actual payment amount + */ + void applyPayment(@NonNull ProjectedAmortizationScheduleModel model, @NonNull LocalDate paymentDate, @NonNull BigDecimal paymentAmount); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java new file mode 100644 index 00000000000..385f04ee1a0 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleModel.java @@ -0,0 +1,430 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.calc; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.apache.fineract.infrastructure.core.serialization.gson.JsonExclude; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; + +/** + * Projected Amortization Schedule model for Working Capital loans. + * + *

Lifecycle

+ *
    + *
  1. {@link #generate} — create initial schedule (at loan creation)
  2. + *
  3. {@link #regenerate} — recalculate with new amounts (at approval / disbursement)
  4. + *
  5. {@link #applyPayment} — record payments by date; schedule rebuilds after each
  6. + *
+ */ +@Getter +@Accessors(fluent = true) +public final class ProjectedAmortizationScheduleModel { + + private static final String MODEL_VERSION = "1"; + + private final Money originationFeeAmount; + private final Money netDisbursementAmount; + private final Money totalPaymentValue; + private final BigDecimal periodPaymentRate; + private final int npvDayCount; + private final LocalDate expectedDisbursementDate; + + /** {@code (TPV × periodPaymentRate) / npvDayCount} — constant across payments. */ + private final Money expectedPaymentAmount; + + /** {@code roundUp((netDisbursementAmount + originationFeeAmount) / expectedPaymentAmount)} */ + private final int loanTerm; + + /** Periodic EIR from {@code RATE(loanTerm, -expectedPayment, netDisbursementAmount)}. */ + private final BigDecimal effectiveInterestRate; + + @JsonExclude + private final MathContext mc; + + @JsonExclude + private final MonetaryCurrency currency; + + @Getter(AccessLevel.NONE) + private final List appliedPayments; + + @Getter(AccessLevel.NONE) + private List payments; + + private ProjectedAmortizationScheduleModel(final Money originationFeeAmount, final Money netDisbursementAmount, + final Money totalPaymentValue, final BigDecimal periodPaymentRate, final int npvDayCount, + final LocalDate expectedDisbursementDate, final Money expectedPaymentAmount, final int loanTerm, + final BigDecimal effectiveInterestRate, final MathContext mc, final MonetaryCurrency currency) { + this.originationFeeAmount = originationFeeAmount; + this.netDisbursementAmount = netDisbursementAmount; + this.totalPaymentValue = totalPaymentValue; + this.periodPaymentRate = periodPaymentRate; + this.npvDayCount = npvDayCount; + this.expectedDisbursementDate = expectedDisbursementDate; + this.expectedPaymentAmount = expectedPaymentAmount; + this.loanTerm = loanTerm; + this.effectiveInterestRate = effectiveInterestRate; + this.mc = mc; + this.currency = currency; + this.appliedPayments = new ArrayList<>(); + rebuildPayments(); + } + + /** + * Creates a skeleton instance for Gson deserialization. Gson will overwrite final fields via reflection; payments + * are restored from JSON directly (no rebuild needed). + */ + public static ProjectedAmortizationScheduleModel forDeserialization(final MathContext mc, final MonetaryCurrency currency) { + return new ProjectedAmortizationScheduleModel(mc, currency); + } + + private ProjectedAmortizationScheduleModel(final MathContext mc, final MonetaryCurrency currency) { + this.originationFeeAmount = null; + this.netDisbursementAmount = null; + this.totalPaymentValue = null; + this.periodPaymentRate = null; + this.npvDayCount = 0; + this.expectedDisbursementDate = null; + this.expectedPaymentAmount = null; + this.loanTerm = 0; + this.effectiveInterestRate = null; + this.mc = mc; + this.currency = currency; + this.appliedPayments = new ArrayList<>(); + this.payments = List.of(); + } + + public List payments() { + return payments; + } + + public static ProjectedAmortizationScheduleModel generate(final BigDecimal originationFeeAmount, final BigDecimal netDisbursementAmount, + final BigDecimal totalPaymentValue, final BigDecimal periodPaymentRate, final int npvDayCount, + final LocalDate expectedDisbursementDate, final MathContext mc, final MonetaryCurrency currency) { + + Objects.requireNonNull(originationFeeAmount, "originationFeeAmount"); + Objects.requireNonNull(netDisbursementAmount, "netDisbursementAmount"); + Objects.requireNonNull(totalPaymentValue, "totalPaymentValue"); + Objects.requireNonNull(periodPaymentRate, "periodPaymentRate"); + Objects.requireNonNull(expectedDisbursementDate, "expectedDisbursementDate"); + Objects.requireNonNull(currency, "currency"); + if (netDisbursementAmount.signum() <= 0) { + throw new IllegalArgumentException("netDisbursementAmount must be positive"); + } + if (npvDayCount <= 0) { + throw new IllegalArgumentException("npvDayCount must be positive"); + } + + final BigDecimal expectedPayment = totalPaymentValue.multiply(periodPaymentRate, mc).divide(BigDecimal.valueOf(npvDayCount), mc); + if (expectedPayment.signum() <= 0) { + throw new IllegalArgumentException("expectedPaymentAmount must be positive (check totalPaymentValue and periodPaymentRate)"); + } + + final int term = netDisbursementAmount.add(originationFeeAmount, mc).divide(expectedPayment, mc).setScale(0, RoundingMode.UP) + .intValueExact(); + if (term <= 0) { + throw new IllegalArgumentException("computed loan term must be positive, got: " + term); + } + + final BigDecimal eir = TvmFunctions.rate(term, expectedPayment.negate(), netDisbursementAmount, mc); + + return new ProjectedAmortizationScheduleModel(Money.of(currency, originationFeeAmount, mc), + Money.of(currency, netDisbursementAmount, mc), Money.of(currency, totalPaymentValue, mc), periodPaymentRate, npvDayCount, + expectedDisbursementDate, Money.of(currency, expectedPayment, mc), term, eir, mc, currency); + } + + public void applyPayment(final LocalDate paymentDate, final BigDecimal amount) { + Objects.requireNonNull(paymentDate, "paymentDate"); + Objects.requireNonNull(amount, "amount"); + final int index = resolvePaymentIndex(paymentDate); + if (index < 0 || index >= loanTerm) { + throw new IllegalArgumentException("paymentDate " + paymentDate + " is outside the valid range [" + + expectedDisbursementDate.plusDays(1) + " .. " + expectedDisbursementDate.plusDays(loanTerm) + "]"); + } + appliedPayments.add(new AppliedPayment(paymentDate, amount)); + rebuildPayments(); + } + + /** Creates a new model with updated parameters, preserving applied payments. */ + public ProjectedAmortizationScheduleModel regenerate(final BigDecimal newDiscountAmount, final BigDecimal newNetAmount, + final LocalDate newStartDate) { + final ProjectedAmortizationScheduleModel newModel = generate(newDiscountAmount, newNetAmount, totalPaymentValue.getAmount(), + periodPaymentRate, npvDayCount, newStartDate, mc, currency); + newModel.appliedPayments.addAll(appliedPayments); + newModel.rebuildPayments(); + return newModel; + } + + private void rebuildPayments() { + final Map paymentsByDate = aggregatePaymentsByDate(); + final List paymentList = buildPaymentList(paymentsByDate); + this.payments = List.copyOf(buildPayments(paymentList, paymentsByDate.size())); + } + + private Map aggregatePaymentsByDate() { + final Map result = new HashMap<>(); + for (final AppliedPayment payment : appliedPayments) { + result.merge(payment.date(), payment.amount(), BigDecimal::add); + } + return result; + } + + private List buildPaymentList(final Map paymentsByDate) { + final List result = new ArrayList<>(loanTerm); + for (int i = 0; i < loanTerm; i++) { + final LocalDate paymentDate = expectedDisbursementDate.plusDays(i + 1); + result.add(paymentsByDate.get(paymentDate)); + } + return result; + } + + private int resolvePaymentIndex(final LocalDate date) { + return (int) ChronoUnit.DAYS.between(expectedDisbursementDate, date) - 1; + } + + private List buildPayments(final List payments, final int appliedCount) { + final BalancesAndAmortizations ba = computeBalancesAndAmortizations(); + final PaymentAnalysis pa = analyzePayments(payments, appliedCount); + final List actualAmortizations = computeActualAmortizations(ba.expectedAmortizations, payments, appliedCount); + final List runningExpected = computeRunningExpectedPayments(pa.excess); + final List tailPayments = new ArrayList<>(); + final BigDecimal tailNpv = buildTailPeriodsAndComputeNpv(tailPayments, pa.shortfall, appliedCount); + final BigDecimal totalNetAmortization = computeTotalNetAmortization(payments, runningExpected, appliedCount, tailNpv); + + final BigDecimal originationFee = originationFeeAmount.getAmount(); + final BigDecimal safeExpectedPayment = MathUtil.negativeToZero(expectedPaymentAmount.getAmount()); + + final List result = new ArrayList<>(loanTerm + 2 + tailPayments.size()); + result.add(createDisbursementPayment(appliedCount)); + + BigDecimal cumulativeActualAmort = BigDecimal.ZERO; + for (int i = 0; i < loanTerm; i++) { + final int periodNo = i + 1; + final boolean hasAppliedAmount = payments.get(i) != null; + final long count = (long) loanTerm + appliedCount - periodNo; + final long paymentsLeft = paymentsLeft(periodNo, appliedCount); + final BigDecimal safeDf = safeDiscountFactor(paymentsLeft); + final BigDecimal safeRunningExpected = MathUtil.negativeToZero(runningExpected.get(i)); + final BigDecimal npvSource = hasAppliedAmount ? payments.get(i) : safeRunningExpected; + final BigDecimal npvValue = MathUtil.negativeToZero(npvSource.multiply(safeDf, mc)); + final BigDecimal safeExpectedAmort = ba.expectedAmortizations.get(i).min(originationFee); + + final BigDecimal netAmortization; + final BigDecimal actualAmortization; + final BigDecimal incomeModification; + + if (hasAppliedAmount) { + actualAmortization = actualAmortizations.get(i); + netAmortization = totalNetAmortization.subtract(cumulativeActualAmort, mc).min(originationFee); + cumulativeActualAmort = cumulativeActualAmort.add(actualAmortization, mc).min(originationFee); + incomeModification = actualAmortization.subtract(safeExpectedAmort, mc); + } else { + netAmortization = BigDecimal.ZERO; + actualAmortization = null; + incomeModification = safeExpectedAmort.negate(); + } + + final BigDecimal deferredBalance = originationFee.subtract(cumulativeActualAmort, mc); + final BigDecimal balance = ba.balances.get(i); + result.add(new ProjectedPayment(periodNo, expectedDisbursementDate.plusDays(periodNo), count, paymentsLeft, + money(safeExpectedPayment), money(safeRunningExpected), safeDf, money(npvValue), money(balance), + money(safeExpectedAmort), money(netAmortization), hasAppliedAmount ? money(payments.get(i)) : null, + actualAmortization != null ? money(actualAmortization) : null, money(incomeModification), money(deferredBalance))); + } + + result.addAll(tailPayments); + + while (result.size() > 1) { + final ProjectedPayment last = result.getLast(); + if (last.forecastPaymentAmount() != null && last.forecastPaymentAmount().isZero()) { + result.removeLast(); + } else { + break; + } + } + + return result; + } + + private ProjectedPayment createDisbursementPayment(final int appliedCount) { + final Money negDisbursement = netDisbursementAmount.negated(mc); + final long count = (long) loanTerm + appliedCount; + return new ProjectedPayment(0, expectedDisbursementDate, count, 0L, negDisbursement, null, BigDecimal.ONE, negDisbursement, + netDisbursementAmount, null, null, null, null, null, originationFeeAmount); + } + + /** + * {@code balance[i] = balance[i-1]×(1+EIR) - expectedPayment}
+ * {@code expectedAmort[i] = balance[i] + expectedPayment - balance[i-1]} + */ + private BalancesAndAmortizations computeBalancesAndAmortizations() { + final BigDecimal onePlusRate = BigDecimal.ONE.add(effectiveInterestRate, mc); + final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); + final List balances = new ArrayList<>(loanTerm); + final List expectedAmortizations = new ArrayList<>(loanTerm); + BigDecimal prevBalance = netDisbursementAmount.getAmount(); + for (int i = 0; i < loanTerm; i++) { + final BigDecimal balance = prevBalance.multiply(onePlusRate, mc).subtract(expectedPayment, mc); + balances.add(balance); + expectedAmortizations.add(balance.add(expectedPayment, mc).subtract(prevBalance, mc)); + prevBalance = balance; + } + return new BalancesAndAmortizations(balances, expectedAmortizations); + } + + private PaymentAnalysis analyzePayments(final List payments, final int appliedCount) { + final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); + BigDecimal shortfall = BigDecimal.ZERO; + BigDecimal excess = BigDecimal.ZERO; + for (int i = 0; i < appliedCount; i++) { + final BigDecimal diff = payments.get(i).subtract(expectedPayment, mc); + if (diff.signum() > 0) { + excess = excess.add(diff, mc); + } else if (diff.signum() < 0) { + shortfall = shortfall.add(diff.negate(), mc); + } + } + return new PaymentAnalysis(shortfall, excess); + } + + /** Cursor-based: each payment consumes {@code actualPayment/expectedPayment} periods of expected amortization. */ + private List computeActualAmortizations(final List expectedAmortizations, final List payments, + final int appliedCount) { + final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); + final List result = new ArrayList<>(appliedCount); + BigDecimal cursor = BigDecimal.ZERO; + for (int i = 0; i < appliedCount; i++) { + final BigDecimal periodsConsumed = payments.get(i).divide(expectedPayment, mc); + result.add(consumeExpectedAmortization(expectedAmortizations, cursor, periodsConsumed)); + cursor = cursor.add(periodsConsumed, mc); + } + return result; + } + + private BigDecimal consumeExpectedAmortization(final List expectedAmortizations, final BigDecimal startPos, + final BigDecimal count) { + if (count.signum() <= 0) { + return BigDecimal.ZERO; + } + BigDecimal sum = BigDecimal.ZERO; + BigDecimal pos = startPos; + BigDecimal remaining = count; + while (remaining.signum() > 0 && pos.intValue() < expectedAmortizations.size()) { + final int periodIndex = pos.intValue(); + final BigDecimal posInPeriod = pos.subtract(BigDecimal.valueOf(periodIndex), mc); + final BigDecimal availableInPeriod = BigDecimal.ONE.subtract(posInPeriod, mc); + final BigDecimal toConsume = remaining.min(availableInPeriod); + sum = sum.add(toConsume.multiply(expectedAmortizations.get(periodIndex), mc), mc); + pos = pos.add(toConsume, mc); + remaining = remaining.subtract(toConsume, mc); + } + return sum; + } + + private List computeRunningExpectedPayments(final BigDecimal excess) { + final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); + final List running = new ArrayList<>(loanTerm); + for (int i = 0; i < loanTerm; i++) { + running.add(expectedPayment); + } + + BigDecimal remainingExcess = excess; + for (int i = loanTerm - 1; i >= 0 && remainingExcess.signum() > 0; i--) { + final BigDecimal reduction = remainingExcess.min(running.get(i)); + running.set(i, running.get(i).subtract(reduction, mc)); + remainingExcess = remainingExcess.subtract(reduction, mc); + } + return running; + } + + private BigDecimal buildTailPeriodsAndComputeNpv(final List tailPayments, final BigDecimal shortfall, + final int appliedCount) { + final BigDecimal expectedPayment = expectedPaymentAmount.getAmount(); + BigDecimal tailNpv = BigDecimal.ZERO; + BigDecimal remaining = shortfall; + int tailIndex = 0; + while (remaining.signum() > 0) { + final int periodNo = loanTerm + tailIndex + 1; + final long dl = paymentsLeft(periodNo, appliedCount); + final BigDecimal df = safeDiscountFactor(dl); + final BigDecimal forecast = remaining.min(expectedPayment); + final BigDecimal npv = MathUtil.negativeToZero(forecast.multiply(df, mc)); + + final long count = (long) loanTerm + appliedCount - periodNo; + tailNpv = tailNpv.add(npv, mc); + tailPayments.add(new ProjectedPayment(periodNo, expectedDisbursementDate.plusDays(periodNo), count, dl, null, money(forecast), + df, money(npv), null, null, money(BigDecimal.ZERO), null, null, null, null)); + + remaining = remaining.subtract(forecast, mc); + tailIndex++; + } + return tailNpv; + } + + /** {@code totalNetAmortization = -netDisbursementAmount + sum(npvSource × DF) + tailNpv} */ + private BigDecimal computeTotalNetAmortization(final List payments, final List runningExpected, + final int appliedCount, final BigDecimal tailNpv) { + BigDecimal total = netDisbursementAmount.getAmount().negate(); + for (int i = 0; i < loanTerm; i++) { + final BigDecimal npvSource = payments.get(i) != null ? payments.get(i) : runningExpected.get(i); + final BigDecimal df = safeDiscountFactor(paymentsLeft(i + 1, appliedCount)); + total = total.add(npvSource.multiply(df, mc), mc); + } + return total.add(tailNpv, mc); + } + + private BigDecimal safeDiscountFactor(final long paymentsLeft) { + final BigDecimal df = TvmFunctions.discountFactor(effectiveInterestRate, paymentsLeft, mc); + return df.signum() <= 0 ? BigDecimal.ONE : df; + } + + private long paymentsLeft(final int periodNumber, final int appliedCount) { + return Math.max(0L, (long) periodNumber - appliedCount); + } + + private Money money(final BigDecimal amount) { + return Money.of(currency, amount, mc); + } + + private record BalancesAndAmortizations(List balances, List expectedAmortizations) { + } + + private record PaymentAnalysis(BigDecimal shortfall, BigDecimal excess) { + } + + public record AppliedPayment(LocalDate date, BigDecimal amount) { + } + + public static String getModelVersion() { + return MODEL_VERSION; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedPayment.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedPayment.java new file mode 100644 index 00000000000..fe4f3def7c4 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedPayment.java @@ -0,0 +1,80 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.calc; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.experimental.Accessors; +import org.apache.fineract.organisation.monetary.domain.Money; + +/** + * Single payment of a Working Capital loan's projected amortization schedule. + */ +@Getter +@Accessors(fluent = true) +@AllArgsConstructor +public class ProjectedPayment { + + /** 1-based payment number (0 = disbursement row). */ + private final int paymentNo; + + private final LocalDate date; + + /** Remaining periods for calculation: {@code loanTerm + appliedPaymentCount - paymentNo}. */ + private final long count; + + /** Exponent for discount factor: {@code DF = 1/(1+EIR)^paymentsLeft}. Zero for paid periods. */ + private final long paymentsLeft; + + /** {@code (TPV × periodRate) / dayCount}; negated disbursement for row 0. */ + private final Money expectedPaymentAmount; + + /** + * Running expected payment: always the expected amount, adjusted for excess from the tail; {@code null} for row 0. + */ + private final Money forecastPaymentAmount; + + /** {@code 1 / (1 + EIR)^paymentsLeft} */ + private final BigDecimal discountFactor; + + /** {@code forecastPayment × discountFactor} */ + private final Money npvValue; + + /** {@code balance[i] = balance[i-1] × (1+EIR) - expectedPayment} */ + private final Money balance; + + /** {@code balance[i] + expectedPayment - balance[i-1]} (equivalent to {@code prevBalance × EIR}) */ + private final Money expectedAmortizationAmount; + + /** First paid: sum of all NPV values; subsequent: {@code netAmort[i-1] - actualAmort[i-1]}. */ + private final Money netAmortizationAmount; + + private final Money actualPaymentAmount; + + /** Cursor-based consumption of expected amortization proportional to payment ratio. */ + private final Money actualAmortizationAmount; + + /** {@code actualAmortization - expectedAmortization} */ + private final Money incomeModification; + + /** {@code deferredBalance[i-1] - actualAmortization[i]} */ + private final Money deferredBalance; +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java new file mode 100644 index 00000000000..34d8a0e18bd --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/calc/TvmFunctions.java @@ -0,0 +1,157 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.calc; + +import java.math.BigDecimal; +import java.math.MathContext; + +/** + * Time Value of Money (TVM) utility functions for working capital loan calculations. + * + *
    + *
  • {@link #rate} — periodic interest rate via Newton-Raphson (Excel RATE equivalent)
  • + *
  • {@link #discountFactor} — present value discount factor: {@code 1 / (1 + r)^days}
  • + *
+ */ +public final class TvmFunctions { + + private static final int MAX_ITERATIONS = 100; + private static final BigDecimal TOLERANCE = new BigDecimal("1E-10"); + private static final BigDecimal DEFAULT_GUESS = new BigDecimal("0.01"); + private static final BigDecimal TWO = BigDecimal.valueOf(2); + + private TvmFunctions() {} + + /** + * Solves for the periodic interest rate in the present-value annuity equation. Equivalent to Excel + * {@code RATE(nper, pmt, pv)} with {@code fv=0, type=0}. + * + *

+ * Finds {@code r} satisfying: {@code pv·(1+r)^n + pmt·((1+r)^n − 1)/r = 0} + * + *

+ * Initial guess is derived via linear approximation: {@code r ≈ 2·(pmt·n + pv) / (pv·n)} + * + * @param nper + * number of periods (must be positive) + * @param pmt + * payment per period (negative = outgoing cash flow) + * @param pv + * present value (positive = loan disbursement) + * @param mc + * math context for precision + * @return the periodic interest rate + * @throws IllegalArgumentException + * if nper <= 0 + * @throws IllegalStateException + * if Newton-Raphson does not converge + */ + public static BigDecimal rate(final int nper, final BigDecimal pmt, final BigDecimal pv, final MathContext mc) { + return rate(nper, pmt, pv, estimateInitialGuess(nper, pmt, pv, mc), mc); + } + + /** + * Solves for the periodic interest rate with an explicit initial guess. + * + * @see #rate(int, BigDecimal, BigDecimal, MathContext) + */ + public static BigDecimal rate(final int nper, final BigDecimal pmt, final BigDecimal pv, final BigDecimal guess, final MathContext mc) { + if (nper <= 0) { + throw new IllegalArgumentException("nper must be positive, got: " + nper); + } + + final BigDecimal n = BigDecimal.valueOf(nper); + + // Zero-rate case: pv + pmt·n ≈ 0 + if (pv.add(pmt.multiply(n, mc), mc).abs().compareTo(TOLERANCE) < 0) { + return BigDecimal.ZERO; + } + + BigDecimal r = guess; + + for (int iter = 0; iter < MAX_ITERATIONS; iter++) { + if (r.signum() == 0) { + r = TOLERANCE; // nudge away from zero to avoid division by zero + } + + final BigDecimal onePlusR = BigDecimal.ONE.add(r, mc); + final BigDecimal compound = onePlusR.pow(nper, mc); // (1+r)^n + final BigDecimal compoundMinusOne = compound.subtract(BigDecimal.ONE, mc); // (1+r)^n − 1 + + // f(r) = pv·(1+r)^n + pmt·((1+r)^n − 1) / r + final BigDecimal f = pv.multiply(compound, mc).add(pmt.multiply(compoundMinusOne, mc).divide(r, mc)); + + // f'(r) = pv·n·(1+r)^(n−1) + pmt·[n·(1+r)^(n−1)·r − ((1+r)^n−1)] / r² + final BigDecimal dCompound = n.multiply(onePlusR.pow(nper - 1, mc), mc); // n·(1+r)^(n−1) + final BigDecimal rSquared = r.multiply(r, mc); + final BigDecimal fPrime = pv.multiply(dCompound, mc) + .add(pmt.multiply(dCompound.multiply(r, mc).subtract(compoundMinusOne, mc), mc).divide(rSquared, mc)); + + if (fPrime.signum() == 0) { + throw new IllegalStateException("RATE: zero derivative at iteration " + iter + ", r=" + r); + } + + final BigDecimal correction = f.divide(fPrime, mc); + r = r.subtract(correction, mc); + + if (correction.abs().compareTo(TOLERANCE) < 0) { + return r; + } + } + + throw new IllegalStateException("RATE did not converge after " + MAX_ITERATIONS + " iterations"); + } + + /** + * Linear approximation for the initial Newton-Raphson guess: {@code r ≈ 2·(pmt·n + pv) / (pv·n)}. Falls back to + * {@value #DEFAULT_GUESS} if the estimate is non-positive. + */ + private static BigDecimal estimateInitialGuess(final int nper, final BigDecimal pmt, final BigDecimal pv, final MathContext mc) { + final BigDecimal n = BigDecimal.valueOf(nper); + final BigDecimal pvTimesN = pv.multiply(n, mc); + if (pvTimesN.signum() == 0) { + return DEFAULT_GUESS; + } + final BigDecimal estimate = pmt.multiply(n, mc).add(pv, mc).multiply(TWO, mc).divide(pvTimesN, mc); + return estimate.signum() > 0 ? estimate : DEFAULT_GUESS; + } + + /** + * Computes the discount factor: {@code 1 / (1 + eir)^days}. + * + * @param eir + * effective interest rate per period + * @param days + * number of periods to discount + * @param mc + * math context for precision + * @return the discount factor (1.0 when days=0) + * @throws IllegalArgumentException + * if days is negative or exceeds {@link Integer#MAX_VALUE} + */ + public static BigDecimal discountFactor(final BigDecimal eir, final long days, final MathContext mc) { + if (days == 0) { + return BigDecimal.ONE; + } + if (days < 0 || days > Integer.MAX_VALUE) { + throw new IllegalArgumentException("days must be in [0, " + Integer.MAX_VALUE + "], got: " + days); + } + return BigDecimal.ONE.divide(BigDecimal.ONE.add(eir, mc).pow((int) days, mc), mc); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleData.java new file mode 100644 index 00000000000..d5b34284f66 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleData.java @@ -0,0 +1,43 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class ProjectedAmortizationScheduleData { + + private final BigDecimal originationFeeAmount; + private final BigDecimal netDisbursementAmount; + private final BigDecimal totalPaymentValue; + private final BigDecimal periodPaymentRate; + private final int npvDayCount; + private final LocalDate expectedDisbursementDate; + private final BigDecimal expectedPaymentAmount; + private final int loanTerm; + private final BigDecimal effectiveInterestRate; + private final List payments; +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleGenerateRequest.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleGenerateRequest.java new file mode 100644 index 00000000000..380f0f228d8 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationScheduleGenerateRequest.java @@ -0,0 +1,39 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +public class ProjectedAmortizationScheduleGenerateRequest { + + private BigDecimal originationFeeAmount; + private BigDecimal netDisbursementAmount; + private BigDecimal totalPaymentValue; + private BigDecimal periodPaymentRate; + private int npvDayCount; + private LocalDate expectedDisbursementDate; + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java new file mode 100644 index 00000000000..34bc396156b --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ProjectedAmortizationSchedulePaymentData.java @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class ProjectedAmortizationSchedulePaymentData { + + private final int paymentNo; + private final LocalDate paymentDate; + private final long count; + private final long paymentsLeft; + private final BigDecimal expectedPaymentAmount; + private final BigDecimal forecastPaymentAmount; + private final BigDecimal discountFactor; + private final BigDecimal npvValue; + private final BigDecimal balance; + private final BigDecimal expectedAmortizationAmount; + private final BigDecimal netAmortizationAmount; + private final BigDecimal actualPaymentAmount; + private final BigDecimal actualAmortizationAmount; + private final BigDecimal incomeModification; + private final BigDecimal deferredBalance; + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/ProjectedAmortizationLoanModel.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/ProjectedAmortizationLoanModel.java new file mode 100644 index 00000000000..4ea373f47f6 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/ProjectedAmortizationLoanModel.java @@ -0,0 +1,57 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; + +@Entity +@Table(name = "m_wc_loan_amortization_model") +@Getter +@Setter +public class ProjectedAmortizationLoanModel extends AbstractPersistableCustom { + + @Version + int version; + + @OneToOne + @JoinColumn(name = "loan_id", nullable = false) + private WorkingCapitalLoan loan; + + @Column(name = "json_model", columnDefinition = "text", nullable = false) + private String jsonModel; + + @Column(name = "business_date", nullable = false) + private LocalDate businessDate; + + @Column(name = "last_modified_on_utc", nullable = false) + private OffsetDateTime lastModifiedDate; + + @Column(name = "json_model_version", nullable = false) + private String jsonModelVersion; +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/ProjectedAmortizationScheduleNotFoundException.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/ProjectedAmortizationScheduleNotFoundException.java new file mode 100644 index 00000000000..7742ce3da21 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/exception/ProjectedAmortizationScheduleNotFoundException.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class ProjectedAmortizationScheduleNotFoundException extends AbstractPlatformResourceNotFoundException { + + public ProjectedAmortizationScheduleNotFoundException(final Long loanId) { + super("error.msg.wc.loan.amortization.schedule.not.found", + "Projected amortization schedule for Working Capital Loan " + loanId + " does not exist", loanId); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java new file mode 100644 index 00000000000..9888c922904 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/mapper/ProjectedAmortizationScheduleMapper.java @@ -0,0 +1,77 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.mapper; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedPayment; +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationSchedulePaymentData; +import org.springframework.stereotype.Component; + +@Component +public class ProjectedAmortizationScheduleMapper { + + private static final int DISPLAY_SCALE = 2; + private static final RoundingMode DISPLAY_ROUNDING = RoundingMode.HALF_UP; + + public ProjectedAmortizationScheduleData toData(final ProjectedAmortizationScheduleModel model) { + final List paymentDataList = model.payments().stream().map(this::toPaymentData).toList(); + + return ProjectedAmortizationScheduleData.builder() // + .originationFeeAmount(roundMoney(model.originationFeeAmount())) // + .netDisbursementAmount(roundMoney(model.netDisbursementAmount())) // + .totalPaymentValue(roundMoney(model.totalPaymentValue())) // + .periodPaymentRate(model.periodPaymentRate()) // + .npvDayCount(model.npvDayCount()) // + .expectedDisbursementDate(model.expectedDisbursementDate()) // + .expectedPaymentAmount(roundMoney(model.expectedPaymentAmount())) // + .loanTerm(model.loanTerm()) // + .effectiveInterestRate(model.effectiveInterestRate()) // + .payments(paymentDataList) // + .build(); + } + + private ProjectedAmortizationSchedulePaymentData toPaymentData(final ProjectedPayment payment) { + return ProjectedAmortizationSchedulePaymentData.builder() // + .paymentNo(payment.paymentNo()) // + .paymentDate(payment.date()) // + .count(payment.count()) // + .paymentsLeft(payment.paymentsLeft()) // + .expectedPaymentAmount(roundMoney(payment.expectedPaymentAmount())) // + .forecastPaymentAmount(roundMoney(payment.forecastPaymentAmount())) // + .discountFactor(payment.discountFactor()) // + .npvValue(roundMoney(payment.npvValue())) // + .balance(roundMoney(payment.balance())) // + .expectedAmortizationAmount(roundMoney(payment.expectedAmortizationAmount())) // + .netAmortizationAmount(roundMoney(payment.netAmortizationAmount())) // + .actualPaymentAmount(roundMoney(payment.actualPaymentAmount())) // + .actualAmortizationAmount(roundMoney(payment.actualAmortizationAmount())) // + .incomeModification(roundMoney(payment.incomeModification())) // + .deferredBalance(roundMoney(payment.deferredBalance())) // + .build(); + } + + private static BigDecimal roundMoney(final Money value) { + return value != null ? value.getAmount().setScale(DISPLAY_SCALE, DISPLAY_ROUNDING) : null; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/ProjectedAmortizationLoanModelRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/ProjectedAmortizationLoanModelRepository.java new file mode 100644 index 00000000000..5177341e25d --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/ProjectedAmortizationLoanModelRepository.java @@ -0,0 +1,31 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.repository; + +import java.util.Optional; +import org.apache.fineract.portfolio.workingcapitalloan.domain.ProjectedAmortizationLoanModel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface ProjectedAmortizationLoanModelRepository + extends JpaSpecificationExecutor, JpaRepository { + + Optional findByLoanId(Long loanId); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserService.java new file mode 100644 index 00000000000..62a1cfacd15 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserService.java @@ -0,0 +1,33 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.math.MathContext; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +public interface ProjectedAmortizationScheduleModelParserService { + + String toJson(@NonNull ProjectedAmortizationScheduleModel model); + + @Nullable + ProjectedAmortizationScheduleModel fromJson(@Nullable String json, @NonNull MathContext mc, @NonNull MonetaryCurrency currency); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserServiceGsonImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserServiceGsonImpl.java new file mode 100644 index 00000000000..3de2eced796 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleModelParserServiceGsonImpl.java @@ -0,0 +1,87 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.InstanceCreator; +import com.google.gson.ToNumberPolicy; +import java.math.MathContext; +import java.time.LocalDate; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.core.serialization.gson.JsonExcludeAnnotationBasedExclusionStrategy; +import org.apache.fineract.infrastructure.core.serialization.gson.LocalDateAdapter; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.serialization.MoneyDeserializer; +import org.apache.fineract.organisation.monetary.serialization.MoneySerializer; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class ProjectedAmortizationScheduleModelParserServiceGsonImpl implements ProjectedAmortizationScheduleModelParserService { + + private final Gson serializer = createSerializer(); + + private static Gson createSerializer() { + return new GsonBuilder() // + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) // + .registerTypeAdapter(Money.class, new MoneySerializer()) // + .setNumberToNumberStrategy(ToNumberPolicy.BIG_DECIMAL) // + .addSerializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()) // + .addDeserializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()) // + .create(); + } + + private static Gson createDeserializer(final MathContext mc, final MonetaryCurrency currency) { + return new GsonBuilder() // + .registerTypeAdapter(LocalDate.class, new LocalDateAdapter().nullSafe()) // + .registerTypeAdapter(Money.class, new MoneyDeserializer(mc, currency)) // + .setNumberToNumberStrategy(ToNumberPolicy.BIG_DECIMAL) // + .registerTypeAdapter(ProjectedAmortizationScheduleModel.class, + (InstanceCreator) type -> ProjectedAmortizationScheduleModel + .forDeserialization(mc, currency)) // + .addSerializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()) // + .addDeserializationExclusionStrategy(new JsonExcludeAnnotationBasedExclusionStrategy()) // + .create(); + } + + @Override + public String toJson(@NonNull final ProjectedAmortizationScheduleModel model) { + return serializer.toJson(model); + } + + @Override + @Nullable + public ProjectedAmortizationScheduleModel fromJson(@Nullable final String json, @NonNull final MathContext mc, + @NonNull final MonetaryCurrency currency) { + if (json == null) { + return null; + } + try { + return createDeserializer(mc, currency).fromJson(json, ProjectedAmortizationScheduleModel.class); + } catch (Exception e) { + log.warn("Failed to parse ProjectedAmortizationScheduleModel JSON. Falling back to null.", e); + return null; + } + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapper.java new file mode 100644 index 00000000000..98c6b95dd93 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapper.java @@ -0,0 +1,34 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.math.MathContext; +import java.util.Optional; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.springframework.lang.NonNull; + +public interface ProjectedAmortizationScheduleRepositoryWrapper { + + Optional readModel(Long loanId, @NonNull MathContext mc, @NonNull MonetaryCurrency currency); + + void writeModel(@NonNull WorkingCapitalLoan loan, @NonNull ProjectedAmortizationScheduleModel model); + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java new file mode 100644 index 00000000000..b0448a73e73 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/ProjectedAmortizationScheduleRepositoryWrapperImpl.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.math.MathContext; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; +import org.apache.fineract.portfolio.workingcapitalloan.domain.ProjectedAmortizationLoanModel; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.repository.ProjectedAmortizationLoanModelRepository; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class ProjectedAmortizationScheduleRepositoryWrapperImpl implements ProjectedAmortizationScheduleRepositoryWrapper { + + private final ProjectedAmortizationLoanModelRepository repository; + private final ProjectedAmortizationScheduleModelParserService parserService; + + @Override + public Optional readModel(final Long loanId, @NonNull final MathContext mc, + @NonNull final MonetaryCurrency currency) { + return repository.findByLoanId(loanId) // + .map(ProjectedAmortizationLoanModel::getJsonModel) // + .map(json -> parserService.fromJson(json, mc, currency)); + } + + @Override + @Transactional + public void writeModel(@NonNull final WorkingCapitalLoan loan, @NonNull final ProjectedAmortizationScheduleModel model) { + final String jsonModel = parserService.toJson(model); + final ProjectedAmortizationLoanModel entity = repository.findByLoanId(loan.getId()).orElseGet(() -> { + final ProjectedAmortizationLoanModel newEntity = new ProjectedAmortizationLoanModel(); + newEntity.setLoan(loan); + return newEntity; + }); + entity.setBusinessDate(ThreadLocalContextUtil.getBusinessDate()); + entity.setLastModifiedDate(DateUtils.getAuditOffsetDateTime()); + entity.setJsonModel(jsonModel); + entity.setJsonModelVersion(ProjectedAmortizationScheduleModel.getModelVersion()); + repository.save(entity); + } + +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadService.java new file mode 100644 index 00000000000..137a1bd30f9 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadService.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleData; + +public interface WorkingCapitalLoanAmortizationScheduleReadService { + + ProjectedAmortizationScheduleData retrieveAmortizationSchedule(Long loanId); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadServiceImpl.java new file mode 100644 index 00000000000..b16d947236a --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleReadServiceImpl.java @@ -0,0 +1,58 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.math.MathContext; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.exception.ProjectedAmortizationScheduleNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.ProjectedAmortizationScheduleMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class WorkingCapitalLoanAmortizationScheduleReadServiceImpl implements WorkingCapitalLoanAmortizationScheduleReadService { + + // TODO: currency should come from loan product once WCL lifecycle is implemented + private static final MonetaryCurrency DEFAULT_CURRENCY = new MonetaryCurrency("USD", 2, null); + + private final WorkingCapitalLoanRepository loanRepository; + private final ProjectedAmortizationScheduleRepositoryWrapper scheduleRepositoryWrapper; + private final ProjectedAmortizationScheduleMapper mapper; + + @Override + public ProjectedAmortizationScheduleData retrieveAmortizationSchedule(final Long loanId) { + if (!loanRepository.existsById(loanId)) { + throw new WorkingCapitalLoanNotFoundException(loanId); + } + + final MathContext mc = MoneyHelper.getMathContext(); + final ProjectedAmortizationScheduleModel model = scheduleRepositoryWrapper.readModel(loanId, mc, DEFAULT_CURRENCY) + .orElseThrow(() -> new ProjectedAmortizationScheduleNotFoundException(loanId)); + + return mapper.toData(model); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java new file mode 100644 index 00000000000..2f6958cbad7 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteService.java @@ -0,0 +1,26 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleGenerateRequest; + +public interface WorkingCapitalLoanAmortizationScheduleWriteService { + + void generateAndSaveAmortizationSchedule(Long loanId, ProjectedAmortizationScheduleGenerateRequest request); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java new file mode 100644 index 00000000000..f8729c0b610 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanAmortizationScheduleWriteServiceImpl.java @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.service; + +import java.math.MathContext; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.workingcapitalloan.calc.ProjectedAmortizationScheduleModel; +import org.apache.fineract.portfolio.workingcapitalloan.data.ProjectedAmortizationScheduleGenerateRequest; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.exception.WorkingCapitalLoanNotFoundException; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +// TODO: This is a temporary testing implementation. In the real flow, the amortization schedule +// will be generated and saved as part of the loan lifecycle (approve/disburse) — not via a +// standalone endpoint. The parameters will come from the loan entity + product, not from the +// request body. Replace this once the full WCL lifecycle is implemented. +@Service +@RequiredArgsConstructor +@Transactional +public class WorkingCapitalLoanAmortizationScheduleWriteServiceImpl implements WorkingCapitalLoanAmortizationScheduleWriteService { + + // TODO: currency should come from loan product once WCL lifecycle is implemented + private static final MonetaryCurrency DEFAULT_CURRENCY = new MonetaryCurrency("USD", 2, null); + + private final WorkingCapitalLoanRepository loanRepository; + private final ProjectedAmortizationScheduleRepositoryWrapper scheduleRepositoryWrapper; + + @Override + public void generateAndSaveAmortizationSchedule(final Long loanId, final ProjectedAmortizationScheduleGenerateRequest request) { + final WorkingCapitalLoan loan = loanRepository.findById(loanId).orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); + + final MathContext mc = MoneyHelper.getMathContext(); + + final ProjectedAmortizationScheduleModel model = ProjectedAmortizationScheduleModel.generate(// + request.getOriginationFeeAmount(), // + request.getNetDisbursementAmount(), // + request.getTotalPaymentValue(), // + request.getPeriodPaymentRate(), // + request.getNpvDayCount(), // + request.getExpectedDisbursementDate(), // + mc, DEFAULT_CURRENCY); + + scheduleRepositoryWrapper.writeModel(loan, model); + } +} diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml index 47ef0f45d5d..d49e648c1d9 100644 --- a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/module-changelog-master.xml @@ -30,4 +30,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0009_wc_loan_amortization_model.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0009_wc_loan_amortization_model.xml new file mode 100644 index 00000000000..90f085f069a --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0009_wc_loan_amortization_model.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java new file mode 100644 index 00000000000..e09a2cf63cb --- /dev/null +++ b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/calc/ProjectedAmortizationScheduleCalculatorTest.java @@ -0,0 +1,2316 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.workingcapitalloan.calc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.junit.jupiter.api.Test; + +class ProjectedAmortizationScheduleCalculatorTest { + + private static final MathContext MC = MathContext.DECIMAL128; + private static final MonetaryCurrency CURRENCY = new MonetaryCurrency("USD", 2, null); + + private static final BigDecimal ORIGINATION_FEE = new BigDecimal("1000"); + private static final BigDecimal NET_DISBURSEMENT = new BigDecimal("9000"); + private static final BigDecimal TPV = new BigDecimal("100000"); + private static final BigDecimal RATE = new BigDecimal("0.18"); + private static final int DAY_COUNT = 360; + private static final LocalDate EXPECTED_DISBURSEMENT_DATE = LocalDate.of(2019, 1, 1); + private static final int TERM = 200; + + private final ProjectedAmortizationScheduleCalculator calculator = new DefaultProjectedAmortizationScheduleCalculator(); + + @Test + void testAddDisbursement_term10_originationFee50_netDisbursement450_then430() { + final BigDecimal originationFee = new BigDecimal("50"); + final BigDecimal initialNetDisbursement = new BigDecimal("450"); + final LocalDate initialDisbursementDate = LocalDate.of(2019, 1, 1); + + final ProjectedAmortizationScheduleModel initial = calculator.generateModel(originationFee, initialNetDisbursement, TPV, RATE, + DAY_COUNT, initialDisbursementDate, MC, CURRENCY); + final ProjectedAmortizationScheduleModel model1 = calculator.addDisbursement(initial, originationFee, initialNetDisbursement, + initialDisbursementDate); + + assertEquals(10, model1.loanTerm()); + assertEquals(11, model1.payments().size()); + + checkInst(model1, 0, 0, LocalDate.of(2019, 1, 1), 10, 0, -450.00, null, null, 1.00000000, -450.00, 450.00, null, null, null, null, + 50.00); + checkInst(model1, 1, 1, LocalDate.of(2019, 1, 2), 9, 1, 50.00, 50.00, null, 0.98074794, 49.04, 408.83, 8.83, 0.00, null, -8.83, + 50.00); + checkInst(model1, 2, 2, LocalDate.of(2019, 1, 3), 8, 2, 50.00, 50.00, null, 0.96186652, 48.09, 366.86, 8.03, 0.00, null, -8.03, + 50.00); + checkInst(model1, 3, 3, LocalDate.of(2019, 1, 4), 7, 3, 50.00, 50.00, null, 0.94334860, 47.17, 324.06, 7.20, 0.00, null, -7.20, + 50.00); + checkInst(model1, 4, 4, LocalDate.of(2019, 1, 5), 6, 4, 50.00, 50.00, null, 0.92518720, 46.26, 280.42, 6.36, 0.00, null, -6.36, + 50.00); + checkInst(model1, 5, 5, LocalDate.of(2019, 1, 6), 5, 5, 50.00, 50.00, null, 0.90737544, 45.37, 235.93, 5.50, 0.00, null, -5.50, + 50.00); + checkInst(model1, 6, 6, LocalDate.of(2019, 1, 7), 4, 6, 50.00, 50.00, null, 0.88990659, 44.50, 190.56, 4.63, 0.00, null, -4.63, + 50.00); + checkInst(model1, 7, 7, LocalDate.of(2019, 1, 8), 3, 7, 50.00, 50.00, null, 0.87277405, 43.64, 144.30, 3.74, 0.00, null, -3.74, + 50.00); + checkInst(model1, 8, 8, LocalDate.of(2019, 1, 9), 2, 8, 50.00, 50.00, null, 0.85597135, 42.80, 97.13, 2.83, 0.00, null, -2.83, + 50.00); + checkInst(model1, 9, 9, LocalDate.of(2019, 1, 10), 1, 9, 50.00, 50.00, null, 0.83949214, 41.97, 49.04, 1.91, 0.00, null, -1.91, + 50.00); + checkInst(model1, 10, 10, LocalDate.of(2019, 1, 11), 0, 10, 50.00, 50.00, null, 0.82333018, 41.17, 0.00, 0.96, 0.00, null, -0.96, + 50.00); + + final BigDecimal newNetDisbursement = new BigDecimal("430"); + final LocalDate newDisbursementDate = LocalDate.of(2019, 1, 5); + + final ProjectedAmortizationScheduleModel model2 = calculator.addDisbursement(model1, originationFee, newNetDisbursement, + newDisbursementDate); + + assertEquals(10, model2.loanTerm()); + assertEquals(11, model2.payments().size()); + + checkInst(model2, 0, 0, LocalDate.of(2019, 1, 5), 10, 0, -430.00, null, null, 1.00000000, -430.00, 430.00, null, null, null, null, + 50.00); + checkInst(model2, 1, 1, LocalDate.of(2019, 1, 6), 9, 1, 50.00, 50.00, null, 0.97237826, 48.62, 392.21, 12.21, 0.00, null, -12.21, + 50.00); + checkInst(model2, 2, 2, LocalDate.of(2019, 1, 7), 8, 2, 50.00, 50.00, null, 0.94551948, 47.28, 353.36, 11.14, 0.00, null, -11.14, + 50.00); + checkInst(model2, 3, 3, LocalDate.of(2019, 1, 8), 7, 3, 50.00, 50.00, null, 0.91940259, 45.97, 313.39, 10.04, 0.00, null, -10.04, + 50.00); + checkInst(model2, 4, 4, LocalDate.of(2019, 1, 9), 6, 4, 50.00, 50.00, null, 0.89400709, 44.70, 272.30, 8.90, 0.00, null, -8.90, + 50.00); + checkInst(model2, 5, 5, LocalDate.of(2019, 1, 10), 5, 5, 50.00, 50.00, null, 0.86931306, 43.47, 230.03, 7.73, 0.00, null, -7.73, + 50.00); + checkInst(model2, 6, 6, LocalDate.of(2019, 1, 11), 4, 6, 50.00, 50.00, null, 0.84530113, 42.27, 186.57, 6.53, 0.00, null, -6.53, + 50.00); + checkInst(model2, 7, 7, LocalDate.of(2019, 1, 12), 3, 7, 50.00, 50.00, null, 0.82195244, 41.10, 141.87, 5.30, 0.00, null, -5.30, + 50.00); + checkInst(model2, 8, 8, LocalDate.of(2019, 1, 13), 2, 8, 50.00, 50.00, null, 0.79924869, 39.96, 95.89, 4.03, 0.00, null, -4.03, + 50.00); + checkInst(model2, 9, 9, LocalDate.of(2019, 1, 14), 1, 9, 50.00, 50.00, null, 0.77717205, 38.86, 48.62, 2.72, 0.00, null, -2.72, + 50.00); + checkInst(model2, 10, 10, LocalDate.of(2019, 1, 15), 0, 10, 50.00, 50.00, null, 0.75570520, 37.79, 0.00, 1.38, 0.00, null, -1.38, + 50.00); + } + + @Test + void testProjectedSchedule_term200_originationFee1000_netDisbursement9000() { + final ProjectedAmortizationScheduleModel model = generateModel(); + + assertEquals(TERM, model.loanTerm()); + + checkInst(model, 0, 0, EXPECTED_DISBURSEMENT_DATE, 200, 0, -9000.00, null, null, 1.00000000, -9000.00, 9000.00, null, null, null, + null, 1000.00); + + checkInst(model, 1, 1, LocalDate.of(2019, 1, 2), 199, 1, 50.00, 50.00, null, 0.99893332, 49.95, 8959.61, 9.61, 0.00, null, -9.61, + 1000.00); + checkInst(model, 2, 2, LocalDate.of(2019, 1, 3), 198, 2, 50.00, 50.00, null, 0.99786779, 49.89, 8919.18, 9.57, 0.00, null, -9.57, + 1000.00); + checkInst(model, 3, 3, LocalDate.of(2019, 1, 4), 197, 3, 50.00, 50.00, null, 0.99680339, 49.84, 8878.70, 9.52, 0.00, null, -9.52, + 1000.00); + checkInst(model, 4, 4, LocalDate.of(2019, 1, 5), 196, 4, 50.00, 50.00, null, 0.99574012, 49.79, 8838.18, 9.48, 0.00, null, -9.48, + 1000.00); + checkInst(model, 5, 5, LocalDate.of(2019, 1, 6), 195, 5, 50.00, 50.00, null, 0.99467799, 49.73, 8797.62, 9.44, 0.00, null, -9.44, + 1000.00); + checkInst(model, 6, 6, LocalDate.of(2019, 1, 7), 194, 6, 50.00, 50.00, null, 0.99361699, 49.68, 8757.01, 9.39, 0.00, null, -9.39, + 1000.00); + checkInst(model, 7, 7, LocalDate.of(2019, 1, 8), 193, 7, 50.00, 50.00, null, 0.99255712, 49.63, 8716.36, 9.35, 0.00, null, -9.35, + 1000.00); + checkInst(model, 8, 8, LocalDate.of(2019, 1, 9), 192, 8, 50.00, 50.00, null, 0.99149839, 49.57, 8675.67, 9.31, 0.00, null, -9.31, + 1000.00); + checkInst(model, 9, 9, LocalDate.of(2019, 1, 10), 191, 9, 50.00, 50.00, null, 0.99044078, 49.52, 8634.94, 9.26, 0.00, null, -9.26, + 1000.00); + checkInst(model, 10, 10, LocalDate.of(2019, 1, 11), 190, 10, 50.00, 50.00, null, 0.98938430, 49.47, 8594.16, 9.22, 0.00, null, + -9.22, 1000.00); + checkInst(model, 11, 11, LocalDate.of(2019, 1, 12), 189, 11, 50.00, 50.00, null, 0.98832895, 49.42, 8553.33, 9.18, 0.00, null, + -9.18, 1000.00); + checkInst(model, 12, 12, LocalDate.of(2019, 1, 13), 188, 12, 50.00, 50.00, null, 0.98727472, 49.36, 8512.47, 9.13, 0.00, null, + -9.13, 1000.00); + checkInst(model, 13, 13, LocalDate.of(2019, 1, 14), 187, 13, 50.00, 50.00, null, 0.98622162, 49.31, 8471.56, 9.09, 0.00, null, + -9.09, 1000.00); + checkInst(model, 14, 14, LocalDate.of(2019, 1, 15), 186, 14, 50.00, 50.00, null, 0.98516964, 49.26, 8430.60, 9.05, 0.00, null, + -9.05, 1000.00); + checkInst(model, 15, 15, LocalDate.of(2019, 1, 16), 185, 15, 50.00, 50.00, null, 0.98411879, 49.21, 8389.61, 9.00, 0.00, null, + -9.00, 1000.00); + checkInst(model, 16, 16, LocalDate.of(2019, 1, 17), 184, 16, 50.00, 50.00, null, 0.98306905, 49.15, 8348.56, 8.96, 0.00, null, + -8.96, 1000.00); + checkInst(model, 17, 17, LocalDate.of(2019, 1, 18), 183, 17, 50.00, 50.00, null, 0.98202044, 49.10, 8307.48, 8.91, 0.00, null, + -8.91, 1000.00); + checkInst(model, 18, 18, LocalDate.of(2019, 1, 19), 182, 18, 50.00, 50.00, null, 0.98097294, 49.05, 8266.35, 8.87, 0.00, null, + -8.87, 1000.00); + checkInst(model, 19, 19, LocalDate.of(2019, 1, 20), 181, 19, 50.00, 50.00, null, 0.97992656, 49.00, 8225.18, 8.83, 0.00, null, + -8.83, 1000.00); + checkInst(model, 20, 20, LocalDate.of(2019, 1, 21), 180, 20, 50.00, 50.00, null, 0.97888129, 48.94, 8183.96, 8.78, 0.00, null, + -8.78, 1000.00); + checkInst(model, 21, 21, LocalDate.of(2019, 1, 22), 179, 21, 50.00, 50.00, null, 0.97783715, 48.89, 8142.70, 8.74, 0.00, null, + -8.74, 1000.00); + checkInst(model, 22, 22, LocalDate.of(2019, 1, 23), 178, 22, 50.00, 50.00, null, 0.97679411, 48.84, 8101.39, 8.69, 0.00, null, + -8.69, 1000.00); + checkInst(model, 23, 23, LocalDate.of(2019, 1, 24), 177, 23, 50.00, 50.00, null, 0.97575219, 48.79, 8060.04, 8.65, 0.00, null, + -8.65, 1000.00); + checkInst(model, 24, 24, LocalDate.of(2019, 1, 25), 176, 24, 50.00, 50.00, null, 0.97471138, 48.74, 8018.65, 8.61, 0.00, null, + -8.61, 1000.00); + checkInst(model, 25, 25, LocalDate.of(2019, 1, 26), 175, 25, 50.00, 50.00, null, 0.97367168, 48.68, 7977.21, 8.56, 0.00, null, + -8.56, 1000.00); + checkInst(model, 26, 26, LocalDate.of(2019, 1, 27), 174, 26, 50.00, 50.00, null, 0.97263309, 48.63, 7935.73, 8.52, 0.00, null, + -8.52, 1000.00); + checkInst(model, 27, 27, LocalDate.of(2019, 1, 28), 173, 27, 50.00, 50.00, null, 0.97159560, 48.58, 7894.21, 8.47, 0.00, null, + -8.47, 1000.00); + checkInst(model, 28, 28, LocalDate.of(2019, 1, 29), 172, 28, 50.00, 50.00, null, 0.97055922, 48.53, 7852.63, 8.43, 0.00, null, + -8.43, 1000.00); + checkInst(model, 29, 29, LocalDate.of(2019, 1, 30), 171, 29, 50.00, 50.00, null, 0.96952395, 48.48, 7811.02, 8.39, 0.00, null, + -8.39, 1000.00); + checkInst(model, 30, 30, LocalDate.of(2019, 1, 31), 170, 30, 50.00, 50.00, null, 0.96848979, 48.42, 7769.36, 8.34, 0.00, null, + -8.34, 1000.00); + checkInst(model, 31, 31, LocalDate.of(2019, 2, 1), 169, 31, 50.00, 50.00, null, 0.96745672, 48.37, 7727.66, 8.30, 0.00, null, -8.30, + 1000.00); + checkInst(model, 32, 32, LocalDate.of(2019, 2, 2), 168, 32, 50.00, 50.00, null, 0.96642476, 48.32, 7685.91, 8.25, 0.00, null, -8.25, + 1000.00); + checkInst(model, 33, 33, LocalDate.of(2019, 2, 3), 167, 33, 50.00, 50.00, null, 0.96539390, 48.27, 7644.12, 8.21, 0.00, null, -8.21, + 1000.00); + checkInst(model, 34, 34, LocalDate.of(2019, 2, 4), 166, 34, 50.00, 50.00, null, 0.96436413, 48.22, 7602.28, 8.16, 0.00, null, -8.16, + 1000.00); + checkInst(model, 35, 35, LocalDate.of(2019, 2, 5), 165, 35, 50.00, 50.00, null, 0.96333547, 48.17, 7560.40, 8.12, 0.00, null, -8.12, + 1000.00); + checkInst(model, 36, 36, LocalDate.of(2019, 2, 6), 164, 36, 50.00, 50.00, null, 0.96230790, 48.12, 7518.47, 8.07, 0.00, null, -8.07, + 1000.00); + checkInst(model, 37, 37, LocalDate.of(2019, 2, 7), 163, 37, 50.00, 50.00, null, 0.96128143, 48.06, 7476.50, 8.03, 0.00, null, -8.03, + 1000.00); + checkInst(model, 38, 38, LocalDate.of(2019, 2, 8), 162, 38, 50.00, 50.00, null, 0.96025606, 48.01, 7434.48, 7.98, 0.00, null, -7.98, + 1000.00); + checkInst(model, 39, 39, LocalDate.of(2019, 2, 9), 161, 39, 50.00, 50.00, null, 0.95923178, 47.96, 7392.42, 7.94, 0.00, null, -7.94, + 1000.00); + checkInst(model, 40, 40, LocalDate.of(2019, 2, 10), 160, 40, 50.00, 50.00, null, 0.95820859, 47.91, 7350.31, 7.89, 0.00, null, + -7.89, 1000.00); + checkInst(model, 41, 41, LocalDate.of(2019, 2, 11), 159, 41, 50.00, 50.00, null, 0.95718649, 47.86, 7308.16, 7.85, 0.00, null, + -7.85, 1000.00); + checkInst(model, 42, 42, LocalDate.of(2019, 2, 12), 158, 42, 50.00, 50.00, null, 0.95616548, 47.81, 7265.97, 7.80, 0.00, null, + -7.80, 1000.00); + checkInst(model, 43, 43, LocalDate.of(2019, 2, 13), 157, 43, 50.00, 50.00, null, 0.95514557, 47.76, 7223.72, 7.76, 0.00, null, + -7.76, 1000.00); + checkInst(model, 44, 44, LocalDate.of(2019, 2, 14), 156, 44, 50.00, 50.00, null, 0.95412674, 47.71, 7181.44, 7.71, 0.00, null, + -7.71, 1000.00); + checkInst(model, 45, 45, LocalDate.of(2019, 2, 15), 155, 45, 50.00, 50.00, null, 0.95310899, 47.66, 7139.11, 7.67, 0.00, null, + -7.67, 1000.00); + checkInst(model, 46, 46, LocalDate.of(2019, 2, 16), 154, 46, 50.00, 50.00, null, 0.95209233, 47.60, 7096.73, 7.62, 0.00, null, + -7.62, 1000.00); + checkInst(model, 47, 47, LocalDate.of(2019, 2, 17), 153, 47, 50.00, 50.00, null, 0.95107676, 47.55, 7054.31, 7.58, 0.00, null, + -7.58, 1000.00); + checkInst(model, 48, 48, LocalDate.of(2019, 2, 18), 152, 48, 50.00, 50.00, null, 0.95006227, 47.50, 7011.84, 7.53, 0.00, null, + -7.53, 1000.00); + checkInst(model, 49, 49, LocalDate.of(2019, 2, 19), 151, 49, 50.00, 50.00, null, 0.94904886, 47.45, 6969.33, 7.49, 0.00, null, + -7.49, 1000.00); + checkInst(model, 50, 50, LocalDate.of(2019, 2, 20), 150, 50, 50.00, 50.00, null, 0.94803653, 47.40, 6926.77, 7.44, 0.00, null, + -7.44, 1000.00); + checkInst(model, 51, 51, LocalDate.of(2019, 2, 21), 149, 51, 50.00, 50.00, null, 0.94702529, 47.35, 6884.17, 7.40, 0.00, null, + -7.40, 1000.00); + checkInst(model, 52, 52, LocalDate.of(2019, 2, 22), 148, 52, 50.00, 50.00, null, 0.94601512, 47.30, 6841.52, 7.35, 0.00, null, + -7.35, 1000.00); + checkInst(model, 53, 53, LocalDate.of(2019, 2, 23), 147, 53, 50.00, 50.00, null, 0.94500603, 47.25, 6798.82, 7.31, 0.00, null, + -7.31, 1000.00); + checkInst(model, 54, 54, LocalDate.of(2019, 2, 24), 146, 54, 50.00, 50.00, null, 0.94399801, 47.20, 6756.08, 7.26, 0.00, null, + -7.26, 1000.00); + checkInst(model, 55, 55, LocalDate.of(2019, 2, 25), 145, 55, 50.00, 50.00, null, 0.94299107, 47.15, 6713.30, 7.21, 0.00, null, + -7.21, 1000.00); + checkInst(model, 56, 56, LocalDate.of(2019, 2, 26), 144, 56, 50.00, 50.00, null, 0.94198521, 47.10, 6670.47, 7.17, 0.00, null, + -7.17, 1000.00); + checkInst(model, 57, 57, LocalDate.of(2019, 2, 27), 143, 57, 50.00, 50.00, null, 0.94098042, 47.05, 6627.59, 7.12, 0.00, null, + -7.12, 1000.00); + checkInst(model, 58, 58, LocalDate.of(2019, 2, 28), 142, 58, 50.00, 50.00, null, 0.93997669, 47.00, 6584.67, 7.08, 0.00, null, + -7.08, 1000.00); + checkInst(model, 59, 59, LocalDate.of(2019, 3, 1), 141, 59, 50.00, 50.00, null, 0.93897404, 46.95, 6541.70, 7.03, 0.00, null, -7.03, + 1000.00); + checkInst(model, 60, 60, LocalDate.of(2019, 3, 2), 140, 60, 50.00, 50.00, null, 0.93797246, 46.90, 6498.68, 6.99, 0.00, null, -6.99, + 1000.00); + checkInst(model, 61, 61, LocalDate.of(2019, 3, 3), 139, 61, 50.00, 50.00, null, 0.93697195, 46.85, 6455.62, 6.94, 0.00, null, -6.94, + 1000.00); + checkInst(model, 62, 62, LocalDate.of(2019, 3, 4), 138, 62, 50.00, 50.00, null, 0.93597251, 46.80, 6412.51, 6.89, 0.00, null, -6.89, + 1000.00); + checkInst(model, 63, 63, LocalDate.of(2019, 3, 5), 137, 63, 50.00, 50.00, null, 0.93497413, 46.75, 6369.36, 6.85, 0.00, null, -6.85, + 1000.00); + checkInst(model, 64, 64, LocalDate.of(2019, 3, 6), 136, 64, 50.00, 50.00, null, 0.93397681, 46.70, 6326.16, 6.80, 0.00, null, -6.80, + 1000.00); + checkInst(model, 65, 65, LocalDate.of(2019, 3, 7), 135, 65, 50.00, 50.00, null, 0.93298056, 46.65, 6282.92, 6.76, 0.00, null, -6.76, + 1000.00); + checkInst(model, 66, 66, LocalDate.of(2019, 3, 8), 134, 66, 50.00, 50.00, null, 0.93198538, 46.60, 6239.63, 6.71, 0.00, null, -6.71, + 1000.00); + checkInst(model, 67, 67, LocalDate.of(2019, 3, 9), 133, 67, 50.00, 50.00, null, 0.93099125, 46.55, 6196.29, 6.66, 0.00, null, -6.66, + 1000.00); + checkInst(model, 68, 68, LocalDate.of(2019, 3, 10), 132, 68, 50.00, 50.00, null, 0.92999818, 46.50, 6152.91, 6.62, 0.00, null, + -6.62, 1000.00); + checkInst(model, 69, 69, LocalDate.of(2019, 3, 11), 131, 69, 50.00, 50.00, null, 0.92900618, 46.45, 6109.48, 6.57, 0.00, null, + -6.57, 1000.00); + checkInst(model, 70, 70, LocalDate.of(2019, 3, 12), 130, 70, 50.00, 50.00, null, 0.92801523, 46.40, 6066.00, 6.52, 0.00, null, + -6.52, 1000.00); + checkInst(model, 71, 71, LocalDate.of(2019, 3, 13), 129, 71, 50.00, 50.00, null, 0.92702534, 46.35, 6022.48, 6.48, 0.00, null, + -6.48, 1000.00); + checkInst(model, 72, 72, LocalDate.of(2019, 3, 14), 128, 72, 50.00, 50.00, null, 0.92603650, 46.30, 5978.91, 6.43, 0.00, null, + -6.43, 1000.00); + checkInst(model, 73, 73, LocalDate.of(2019, 3, 15), 127, 73, 50.00, 50.00, null, 0.92504872, 46.25, 5935.29, 6.38, 0.00, null, + -6.38, 1000.00); + checkInst(model, 74, 74, LocalDate.of(2019, 3, 16), 126, 74, 50.00, 50.00, null, 0.92406200, 46.20, 5891.63, 6.34, 0.00, null, + -6.34, 1000.00); + checkInst(model, 75, 75, LocalDate.of(2019, 3, 17), 125, 75, 50.00, 50.00, null, 0.92307632, 46.15, 5847.92, 6.29, 0.00, null, + -6.29, 1000.00); + checkInst(model, 76, 76, LocalDate.of(2019, 3, 18), 124, 76, 50.00, 50.00, null, 0.92209170, 46.10, 5804.17, 6.24, 0.00, null, + -6.24, 1000.00); + checkInst(model, 77, 77, LocalDate.of(2019, 3, 19), 123, 77, 50.00, 50.00, null, 0.92110813, 46.06, 5760.36, 6.20, 0.00, null, + -6.20, 1000.00); + checkInst(model, 78, 78, LocalDate.of(2019, 3, 20), 122, 78, 50.00, 50.00, null, 0.92012560, 46.01, 5716.52, 6.15, 0.00, null, + -6.15, 1000.00); + checkInst(model, 79, 79, LocalDate.of(2019, 3, 21), 121, 79, 50.00, 50.00, null, 0.91914413, 45.96, 5672.62, 6.10, 0.00, null, + -6.10, 1000.00); + checkInst(model, 80, 80, LocalDate.of(2019, 3, 22), 120, 80, 50.00, 50.00, null, 0.91816370, 45.91, 5628.68, 6.06, 0.00, null, + -6.06, 1000.00); + checkInst(model, 81, 81, LocalDate.of(2019, 3, 23), 119, 81, 50.00, 50.00, null, 0.91718432, 45.86, 5584.69, 6.01, 0.00, null, + -6.01, 1000.00); + checkInst(model, 82, 82, LocalDate.of(2019, 3, 24), 118, 82, 50.00, 50.00, null, 0.91620598, 45.81, 5540.65, 5.96, 0.00, null, + -5.96, 1000.00); + checkInst(model, 83, 83, LocalDate.of(2019, 3, 25), 117, 83, 50.00, 50.00, null, 0.91522868, 45.76, 5496.57, 5.92, 0.00, null, + -5.92, 1000.00); + checkInst(model, 84, 84, LocalDate.of(2019, 3, 26), 116, 84, 50.00, 50.00, null, 0.91425243, 45.71, 5452.44, 5.87, 0.00, null, + -5.87, 1000.00); + checkInst(model, 85, 85, LocalDate.of(2019, 3, 27), 115, 85, 50.00, 50.00, null, 0.91327722, 45.66, 5408.26, 5.82, 0.00, null, + -5.82, 1000.00); + checkInst(model, 86, 86, LocalDate.of(2019, 3, 28), 114, 86, 50.00, 50.00, null, 0.91230305, 45.62, 5364.03, 5.78, 0.00, null, + -5.78, 1000.00); + checkInst(model, 87, 87, LocalDate.of(2019, 3, 29), 113, 87, 50.00, 50.00, null, 0.91132992, 45.57, 5319.76, 5.73, 0.00, null, + -5.73, 1000.00); + checkInst(model, 88, 88, LocalDate.of(2019, 3, 30), 112, 88, 50.00, 50.00, null, 0.91035783, 45.52, 5275.44, 5.68, 0.00, null, + -5.68, 1000.00); + checkInst(model, 89, 89, LocalDate.of(2019, 3, 31), 111, 89, 50.00, 50.00, null, 0.90938677, 45.47, 5231.08, 5.63, 0.00, null, + -5.63, 1000.00); + checkInst(model, 90, 90, LocalDate.of(2019, 4, 1), 110, 90, 50.00, 50.00, null, 0.90841675, 45.42, 5186.66, 5.59, 0.00, null, -5.59, + 1000.00); + checkInst(model, 91, 91, LocalDate.of(2019, 4, 2), 109, 91, 50.00, 50.00, null, 0.90744776, 45.37, 5142.20, 5.54, 0.00, null, -5.54, + 1000.00); + checkInst(model, 92, 92, LocalDate.of(2019, 4, 3), 108, 92, 50.00, 50.00, null, 0.90647981, 45.32, 5097.69, 5.49, 0.00, null, -5.49, + 1000.00); + checkInst(model, 93, 93, LocalDate.of(2019, 4, 4), 107, 93, 50.00, 50.00, null, 0.90551289, 45.28, 5053.13, 5.44, 0.00, null, -5.44, + 1000.00); + checkInst(model, 94, 94, LocalDate.of(2019, 4, 5), 106, 94, 50.00, 50.00, null, 0.90454700, 45.23, 5008.53, 5.40, 0.00, null, -5.40, + 1000.00); + checkInst(model, 95, 95, LocalDate.of(2019, 4, 6), 105, 95, 50.00, 50.00, null, 0.90358215, 45.18, 4963.88, 5.35, 0.00, null, -5.35, + 1000.00); + checkInst(model, 96, 96, LocalDate.of(2019, 4, 7), 104, 96, 50.00, 50.00, null, 0.90261832, 45.13, 4919.18, 5.30, 0.00, null, -5.30, + 1000.00); + checkInst(model, 97, 97, LocalDate.of(2019, 4, 8), 103, 97, 50.00, 50.00, null, 0.90165552, 45.08, 4874.43, 5.25, 0.00, null, -5.25, + 1000.00); + checkInst(model, 98, 98, LocalDate.of(2019, 4, 9), 102, 98, 50.00, 50.00, null, 0.90069374, 45.03, 4829.64, 5.20, 0.00, null, -5.20, + 1000.00); + checkInst(model, 99, 99, LocalDate.of(2019, 4, 10), 101, 99, 50.00, 50.00, null, 0.89973299, 44.99, 4784.79, 5.16, 0.00, null, + -5.16, 1000.00); + checkInst(model, 100, 100, LocalDate.of(2019, 4, 11), 100, 100, 50.00, 50.00, null, 0.89877327, 44.94, 4739.90, 5.11, 0.00, null, + -5.11, 1000.00); + checkInst(model, 101, 101, LocalDate.of(2019, 4, 12), 99, 101, 50.00, 50.00, null, 0.89781457, 44.89, 4694.96, 5.06, 0.00, null, + -5.06, 1000.00); + checkInst(model, 102, 102, LocalDate.of(2019, 4, 13), 98, 102, 50.00, 50.00, null, 0.89685689, 44.84, 4649.98, 5.01, 0.00, null, + -5.01, 1000.00); + checkInst(model, 103, 103, LocalDate.of(2019, 4, 14), 97, 103, 50.00, 50.00, null, 0.89590024, 44.80, 4604.94, 4.97, 0.00, null, + -4.97, 1000.00); + checkInst(model, 104, 104, LocalDate.of(2019, 4, 15), 96, 104, 50.00, 50.00, null, 0.89494460, 44.75, 4559.86, 4.92, 0.00, null, + -4.92, 1000.00); + checkInst(model, 105, 105, LocalDate.of(2019, 4, 16), 95, 105, 50.00, 50.00, null, 0.89398999, 44.70, 4514.73, 4.87, 0.00, null, + -4.87, 1000.00); + checkInst(model, 106, 106, LocalDate.of(2019, 4, 17), 94, 106, 50.00, 50.00, null, 0.89303639, 44.65, 4469.55, 4.82, 0.00, null, + -4.82, 1000.00); + checkInst(model, 107, 107, LocalDate.of(2019, 4, 18), 93, 107, 50.00, 50.00, null, 0.89208381, 44.60, 4424.32, 4.77, 0.00, null, + -4.77, 1000.00); + checkInst(model, 108, 108, LocalDate.of(2019, 4, 19), 92, 108, 50.00, 50.00, null, 0.89113225, 44.56, 4379.05, 4.72, 0.00, null, + -4.72, 1000.00); + checkInst(model, 109, 109, LocalDate.of(2019, 4, 20), 91, 109, 50.00, 50.00, null, 0.89018170, 44.51, 4333.72, 4.68, 0.00, null, + -4.68, 1000.00); + checkInst(model, 110, 110, LocalDate.of(2019, 4, 21), 90, 110, 50.00, 50.00, null, 0.88923216, 44.46, 4288.35, 4.63, 0.00, null, + -4.63, 1000.00); + checkInst(model, 111, 111, LocalDate.of(2019, 4, 22), 89, 111, 50.00, 50.00, null, 0.88828364, 44.41, 4242.93, 4.58, 0.00, null, + -4.58, 1000.00); + checkInst(model, 112, 112, LocalDate.of(2019, 4, 23), 88, 112, 50.00, 50.00, null, 0.88733613, 44.37, 4197.46, 4.53, 0.00, null, + -4.53, 1000.00); + checkInst(model, 113, 113, LocalDate.of(2019, 4, 24), 87, 113, 50.00, 50.00, null, 0.88638963, 44.32, 4151.94, 4.48, 0.00, null, + -4.48, 1000.00); + checkInst(model, 114, 114, LocalDate.of(2019, 4, 25), 86, 114, 50.00, 50.00, null, 0.88544414, 44.27, 4106.38, 4.43, 0.00, null, + -4.43, 1000.00); + checkInst(model, 115, 115, LocalDate.of(2019, 4, 26), 85, 115, 50.00, 50.00, null, 0.88449966, 44.22, 4060.76, 4.38, 0.00, null, + -4.38, 1000.00); + checkInst(model, 116, 116, LocalDate.of(2019, 4, 27), 84, 116, 50.00, 50.00, null, 0.88355619, 44.18, 4015.10, 4.34, 0.00, null, + -4.34, 1000.00); + checkInst(model, 117, 117, LocalDate.of(2019, 4, 28), 83, 117, 50.00, 50.00, null, 0.88261372, 44.13, 3969.38, 4.29, 0.00, null, + -4.29, 1000.00); + checkInst(model, 118, 118, LocalDate.of(2019, 4, 29), 82, 118, 50.00, 50.00, null, 0.88167226, 44.08, 3923.62, 4.24, 0.00, null, + -4.24, 1000.00); + checkInst(model, 119, 119, LocalDate.of(2019, 4, 30), 81, 119, 50.00, 50.00, null, 0.88073180, 44.04, 3877.81, 4.19, 0.00, null, + -4.19, 1000.00); + checkInst(model, 120, 120, LocalDate.of(2019, 5, 1), 80, 120, 50.00, 50.00, null, 0.87979234, 43.99, 3831.95, 4.14, 0.00, null, + -4.14, 1000.00); + checkInst(model, 121, 121, LocalDate.of(2019, 5, 2), 79, 121, 50.00, 50.00, null, 0.87885389, 43.94, 3786.04, 4.09, 0.00, null, + -4.09, 1000.00); + checkInst(model, 122, 122, LocalDate.of(2019, 5, 3), 78, 122, 50.00, 50.00, null, 0.87791644, 43.90, 3740.09, 4.04, 0.00, null, + -4.04, 1000.00); + checkInst(model, 123, 123, LocalDate.of(2019, 5, 4), 77, 123, 50.00, 50.00, null, 0.87697999, 43.85, 3694.08, 3.99, 0.00, null, + -3.99, 1000.00); + checkInst(model, 124, 124, LocalDate.of(2019, 5, 5), 76, 124, 50.00, 50.00, null, 0.87604453, 43.80, 3648.03, 3.94, 0.00, null, + -3.94, 1000.00); + checkInst(model, 125, 125, LocalDate.of(2019, 5, 6), 75, 125, 50.00, 50.00, null, 0.87511008, 43.76, 3601.92, 3.90, 0.00, null, + -3.90, 1000.00); + checkInst(model, 126, 126, LocalDate.of(2019, 5, 7), 74, 126, 50.00, 50.00, null, 0.87417662, 43.71, 3555.77, 3.85, 0.00, null, + -3.85, 1000.00); + checkInst(model, 127, 127, LocalDate.of(2019, 5, 8), 73, 127, 50.00, 50.00, null, 0.87324416, 43.66, 3509.56, 3.80, 0.00, null, + -3.80, 1000.00); + checkInst(model, 128, 128, LocalDate.of(2019, 5, 9), 72, 128, 50.00, 50.00, null, 0.87231269, 43.62, 3463.31, 3.75, 0.00, null, + -3.75, 1000.00); + checkInst(model, 129, 129, LocalDate.of(2019, 5, 10), 71, 129, 50.00, 50.00, null, 0.87138221, 43.57, 3417.01, 3.70, 0.00, null, + -3.70, 1000.00); + checkInst(model, 130, 130, LocalDate.of(2019, 5, 11), 70, 130, 50.00, 50.00, null, 0.87045273, 43.52, 3370.66, 3.65, 0.00, null, + -3.65, 1000.00); + checkInst(model, 131, 131, LocalDate.of(2019, 5, 12), 69, 131, 50.00, 50.00, null, 0.86952424, 43.48, 3324.26, 3.60, 0.00, null, + -3.60, 1000.00); + checkInst(model, 132, 132, LocalDate.of(2019, 5, 13), 68, 132, 50.00, 50.00, null, 0.86859674, 43.43, 3277.81, 3.55, 0.00, null, + -3.55, 1000.00); + checkInst(model, 133, 133, LocalDate.of(2019, 5, 14), 67, 133, 50.00, 50.00, null, 0.86767023, 43.38, 3231.31, 3.50, 0.00, null, + -3.50, 1000.00); + checkInst(model, 134, 134, LocalDate.of(2019, 5, 15), 66, 134, 50.00, 50.00, null, 0.86674471, 43.34, 3184.76, 3.45, 0.00, null, + -3.45, 1000.00); + checkInst(model, 135, 135, LocalDate.of(2019, 5, 16), 65, 135, 50.00, 50.00, null, 0.86582017, 43.29, 3138.16, 3.40, 0.00, null, + -3.40, 1000.00); + checkInst(model, 136, 136, LocalDate.of(2019, 5, 17), 64, 136, 50.00, 50.00, null, 0.86489662, 43.24, 3091.51, 3.35, 0.00, null, + -3.35, 1000.00); + checkInst(model, 137, 137, LocalDate.of(2019, 5, 18), 63, 137, 50.00, 50.00, null, 0.86397406, 43.20, 3044.81, 3.30, 0.00, null, + -3.30, 1000.00); + checkInst(model, 138, 138, LocalDate.of(2019, 5, 19), 62, 138, 50.00, 50.00, null, 0.86305248, 43.15, 2998.06, 3.25, 0.00, null, + -3.25, 1000.00); + checkInst(model, 139, 139, LocalDate.of(2019, 5, 20), 61, 139, 50.00, 50.00, null, 0.86213188, 43.11, 2951.26, 3.20, 0.00, null, + -3.20, 1000.00); + checkInst(model, 140, 140, LocalDate.of(2019, 5, 21), 60, 140, 50.00, 50.00, null, 0.86121227, 43.06, 2904.42, 3.15, 0.00, null, + -3.15, 1000.00); + checkInst(model, 141, 141, LocalDate.of(2019, 5, 22), 59, 141, 50.00, 50.00, null, 0.86029363, 43.01, 2857.52, 3.10, 0.00, null, + -3.10, 1000.00); + checkInst(model, 142, 142, LocalDate.of(2019, 5, 23), 58, 142, 50.00, 50.00, null, 0.85937598, 42.97, 2810.57, 3.05, 0.00, null, + -3.05, 1000.00); + checkInst(model, 143, 143, LocalDate.of(2019, 5, 24), 57, 143, 50.00, 50.00, null, 0.85845930, 42.92, 2763.57, 3.00, 0.00, null, + -3.00, 1000.00); + checkInst(model, 144, 144, LocalDate.of(2019, 5, 25), 56, 144, 50.00, 50.00, null, 0.85754361, 42.88, 2716.52, 2.95, 0.00, null, + -2.95, 1000.00); + checkInst(model, 145, 145, LocalDate.of(2019, 5, 26), 55, 145, 50.00, 50.00, null, 0.85662889, 42.83, 2669.42, 2.90, 0.00, null, + -2.90, 1000.00); + checkInst(model, 146, 146, LocalDate.of(2019, 5, 27), 54, 146, 50.00, 50.00, null, 0.85571514, 42.79, 2622.27, 2.85, 0.00, null, + -2.85, 1000.00); + checkInst(model, 147, 147, LocalDate.of(2019, 5, 28), 53, 147, 50.00, 50.00, null, 0.85480237, 42.74, 2575.07, 2.80, 0.00, null, + -2.80, 1000.00); + checkInst(model, 148, 148, LocalDate.of(2019, 5, 29), 52, 148, 50.00, 50.00, null, 0.85389057, 42.69, 2527.82, 2.75, 0.00, null, + -2.75, 1000.00); + checkInst(model, 149, 149, LocalDate.of(2019, 5, 30), 51, 149, 50.00, 50.00, null, 0.85297975, 42.65, 2480.52, 2.70, 0.00, null, + -2.70, 1000.00); + checkInst(model, 150, 150, LocalDate.of(2019, 5, 31), 50, 150, 50.00, 50.00, null, 0.85206990, 42.60, 2433.17, 2.65, 0.00, null, + -2.65, 1000.00); + checkInst(model, 151, 151, LocalDate.of(2019, 6, 1), 49, 151, 50.00, 50.00, null, 0.85116101, 42.56, 2385.77, 2.60, 0.00, null, + -2.60, 1000.00); + checkInst(model, 152, 152, LocalDate.of(2019, 6, 2), 48, 152, 50.00, 50.00, null, 0.85025310, 42.51, 2338.31, 2.55, 0.00, null, + -2.55, 1000.00); + checkInst(model, 153, 153, LocalDate.of(2019, 6, 3), 47, 153, 50.00, 50.00, null, 0.84934616, 42.47, 2290.81, 2.50, 0.00, null, + -2.50, 1000.00); + checkInst(model, 154, 154, LocalDate.of(2019, 6, 4), 46, 154, 50.00, 50.00, null, 0.84844018, 42.42, 2243.26, 2.45, 0.00, null, + -2.45, 1000.00); + checkInst(model, 155, 155, LocalDate.of(2019, 6, 5), 45, 155, 50.00, 50.00, null, 0.84753517, 42.38, 2195.65, 2.40, 0.00, null, + -2.40, 1000.00); + checkInst(model, 156, 156, LocalDate.of(2019, 6, 6), 44, 156, 50.00, 50.00, null, 0.84663113, 42.33, 2148.00, 2.34, 0.00, null, + -2.34, 1000.00); + checkInst(model, 157, 157, LocalDate.of(2019, 6, 7), 43, 157, 50.00, 50.00, null, 0.84572805, 42.29, 2100.29, 2.29, 0.00, null, + -2.29, 1000.00); + checkInst(model, 158, 158, LocalDate.of(2019, 6, 8), 42, 158, 50.00, 50.00, null, 0.84482593, 42.24, 2052.53, 2.24, 0.00, null, + -2.24, 1000.00); + checkInst(model, 159, 159, LocalDate.of(2019, 6, 9), 41, 159, 50.00, 50.00, null, 0.84392477, 42.20, 2004.73, 2.19, 0.00, null, + -2.19, 1000.00); + checkInst(model, 160, 160, LocalDate.of(2019, 6, 10), 40, 160, 50.00, 50.00, null, 0.84302458, 42.15, 1956.87, 2.14, 0.00, null, + -2.14, 1000.00); + checkInst(model, 161, 161, LocalDate.of(2019, 6, 11), 39, 161, 50.00, 50.00, null, 0.84212535, 42.11, 1908.96, 2.09, 0.00, null, + -2.09, 1000.00); + checkInst(model, 162, 162, LocalDate.of(2019, 6, 12), 38, 162, 50.00, 50.00, null, 0.84122707, 42.06, 1860.99, 2.04, 0.00, null, + -2.04, 1000.00); + checkInst(model, 163, 163, LocalDate.of(2019, 6, 13), 37, 163, 50.00, 50.00, null, 0.84032975, 42.02, 1812.98, 1.99, 0.00, null, + -1.99, 1000.00); + checkInst(model, 164, 164, LocalDate.of(2019, 6, 14), 36, 164, 50.00, 50.00, null, 0.83943340, 41.97, 1764.92, 1.94, 0.00, null, + -1.94, 1000.00); + checkInst(model, 165, 165, LocalDate.of(2019, 6, 15), 35, 165, 50.00, 50.00, null, 0.83853799, 41.93, 1716.80, 1.88, 0.00, null, + -1.88, 1000.00); + checkInst(model, 166, 166, LocalDate.of(2019, 6, 16), 34, 166, 50.00, 50.00, null, 0.83764354, 41.88, 1668.64, 1.83, 0.00, null, + -1.83, 1000.00); + checkInst(model, 167, 167, LocalDate.of(2019, 6, 17), 33, 167, 50.00, 50.00, null, 0.83675005, 41.84, 1620.42, 1.78, 0.00, null, + -1.78, 1000.00); + checkInst(model, 168, 168, LocalDate.of(2019, 6, 18), 32, 168, 50.00, 50.00, null, 0.83585751, 41.79, 1572.15, 1.73, 0.00, null, + -1.73, 1000.00); + checkInst(model, 169, 169, LocalDate.of(2019, 6, 19), 31, 169, 50.00, 50.00, null, 0.83496592, 41.75, 1523.83, 1.68, 0.00, null, + -1.68, 1000.00); + checkInst(model, 170, 170, LocalDate.of(2019, 6, 20), 30, 170, 50.00, 50.00, null, 0.83407528, 41.70, 1475.45, 1.63, 0.00, null, + -1.63, 1000.00); + checkInst(model, 171, 171, LocalDate.of(2019, 6, 21), 29, 171, 50.00, 50.00, null, 0.83318560, 41.66, 1427.03, 1.58, 0.00, null, + -1.58, 1000.00); + checkInst(model, 172, 172, LocalDate.of(2019, 6, 22), 28, 172, 50.00, 50.00, null, 0.83229686, 41.61, 1378.55, 1.52, 0.00, null, + -1.52, 1000.00); + checkInst(model, 173, 173, LocalDate.of(2019, 6, 23), 27, 173, 50.00, 50.00, null, 0.83140907, 41.57, 1330.02, 1.47, 0.00, null, + -1.47, 1000.00); + checkInst(model, 174, 174, LocalDate.of(2019, 6, 24), 26, 174, 50.00, 50.00, null, 0.83052222, 41.53, 1281.45, 1.42, 0.00, null, + -1.42, 1000.00); + checkInst(model, 175, 175, LocalDate.of(2019, 6, 25), 25, 175, 50.00, 50.00, null, 0.82963633, 41.48, 1232.81, 1.37, 0.00, null, + -1.37, 1000.00); + checkInst(model, 176, 176, LocalDate.of(2019, 6, 26), 24, 176, 50.00, 50.00, null, 0.82875137, 41.44, 1184.13, 1.32, 0.00, null, + -1.32, 1000.00); + checkInst(model, 177, 177, LocalDate.of(2019, 6, 27), 23, 177, 50.00, 50.00, null, 0.82786736, 41.39, 1135.39, 1.26, 0.00, null, + -1.26, 1000.00); + checkInst(model, 178, 178, LocalDate.of(2019, 6, 28), 22, 178, 50.00, 50.00, null, 0.82698430, 41.35, 1086.61, 1.21, 0.00, null, + -1.21, 1000.00); + checkInst(model, 179, 179, LocalDate.of(2019, 6, 29), 21, 179, 50.00, 50.00, null, 0.82610217, 41.31, 1037.77, 1.16, 0.00, null, + -1.16, 1000.00); + checkInst(model, 180, 180, LocalDate.of(2019, 6, 30), 20, 180, 50.00, 50.00, null, 0.82522099, 41.26, 988.88, 1.11, 0.00, null, + -1.11, 1000.00); + checkInst(model, 181, 181, LocalDate.of(2019, 7, 1), 19, 181, 50.00, 50.00, null, 0.82434075, 41.22, 939.93, 1.06, 0.00, null, + -1.06, 1000.00); + checkInst(model, 182, 182, LocalDate.of(2019, 7, 2), 18, 182, 50.00, 50.00, null, 0.82346144, 41.17, 890.93, 1.00, 0.00, null, + -1.00, 1000.00); + checkInst(model, 183, 183, LocalDate.of(2019, 7, 3), 17, 183, 50.00, 50.00, null, 0.82258308, 41.13, 841.89, 0.95, 0.00, null, + -0.95, 1000.00); + checkInst(model, 184, 184, LocalDate.of(2019, 7, 4), 16, 184, 50.00, 50.00, null, 0.82170565, 41.09, 792.79, 0.90, 0.00, null, + -0.90, 1000.00); + checkInst(model, 185, 185, LocalDate.of(2019, 7, 5), 15, 185, 50.00, 50.00, null, 0.82082916, 41.04, 743.63, 0.85, 0.00, null, + -0.85, 1000.00); + checkInst(model, 186, 186, LocalDate.of(2019, 7, 6), 14, 186, 50.00, 50.00, null, 0.81995360, 41.00, 694.43, 0.79, 0.00, null, + -0.79, 1000.00); + checkInst(model, 187, 187, LocalDate.of(2019, 7, 7), 13, 187, 50.00, 50.00, null, 0.81907897, 40.95, 645.17, 0.74, 0.00, null, + -0.74, 1000.00); + checkInst(model, 188, 188, LocalDate.of(2019, 7, 8), 12, 188, 50.00, 50.00, null, 0.81820528, 40.91, 595.86, 0.69, 0.00, null, + -0.69, 1000.00); + checkInst(model, 189, 189, LocalDate.of(2019, 7, 9), 11, 189, 50.00, 50.00, null, 0.81733252, 40.87, 546.49, 0.64, 0.00, null, + -0.64, 1000.00); + checkInst(model, 190, 190, LocalDate.of(2019, 7, 10), 10, 190, 50.00, 50.00, null, 0.81646069, 40.82, 497.08, 0.58, 0.00, null, + -0.58, 1000.00); + checkInst(model, 191, 191, LocalDate.of(2019, 7, 11), 9, 191, 50.00, 50.00, null, 0.81558979, 40.78, 447.61, 0.53, 0.00, null, + -0.53, 1000.00); + checkInst(model, 192, 192, LocalDate.of(2019, 7, 12), 8, 192, 50.00, 50.00, null, 0.81471983, 40.74, 398.08, 0.48, 0.00, null, + -0.48, 1000.00); + checkInst(model, 193, 193, LocalDate.of(2019, 7, 13), 7, 193, 50.00, 50.00, null, 0.81385078, 40.69, 348.51, 0.43, 0.00, null, + -0.43, 1000.00); + checkInst(model, 194, 194, LocalDate.of(2019, 7, 14), 6, 194, 50.00, 50.00, null, 0.81298267, 40.65, 298.88, 0.37, 0.00, null, + -0.37, 1000.00); + checkInst(model, 195, 195, LocalDate.of(2019, 7, 15), 5, 195, 50.00, 50.00, null, 0.81211548, 40.61, 249.20, 0.32, 0.00, null, + -0.32, 1000.00); + checkInst(model, 196, 196, LocalDate.of(2019, 7, 16), 4, 196, 50.00, 50.00, null, 0.81124922, 40.56, 199.47, 0.27, 0.00, null, + -0.27, 1000.00); + checkInst(model, 197, 197, LocalDate.of(2019, 7, 17), 3, 197, 50.00, 50.00, null, 0.81038388, 40.52, 149.68, 0.21, 0.00, null, + -0.21, 1000.00); + checkInst(model, 198, 198, LocalDate.of(2019, 7, 18), 2, 198, 50.00, 50.00, null, 0.80951946, 40.48, 99.84, 0.16, 0.00, null, -0.16, + 1000.00); + checkInst(model, 199, 199, LocalDate.of(2019, 7, 19), 1, 199, 50.00, 50.00, null, 0.80865597, 40.43, 49.95, 0.11, 0.00, null, -0.11, + 1000.00); + checkInst(model, 200, 200, LocalDate.of(2019, 7, 20), 0, 200, 50.00, 50.00, null, 0.80779339, 40.39, 0.00, 0.05, 0.00, null, -0.05, + 1000.00); + } + + @Test + void testOnTimePayment_term200_originationFee1000_netDisbursement9000_pay50_50() { + final ProjectedAmortizationScheduleModel model = generateModel(); + + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(1), new BigDecimal("50")); + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(2), new BigDecimal("50")); + + checkInst(model, 0, 0, EXPECTED_DISBURSEMENT_DATE, 202, 0, -9000.00, null, null, 1.00000000, -9000.00, 9000.00, null, null, null, + null, 1000.00); + + checkInst(model, 1, 1, LocalDate.of(2019, 1, 2), 201, 0, 50.00, 50.00, 50.00, 1.00000000, 50.00, 8959.61, 9.61, 19.18, 9.61, 0.00, + 990.39); + checkInst(model, 2, 2, LocalDate.of(2019, 1, 3), 200, 0, 50.00, 50.00, 50.00, 1.00000000, 50.00, 8919.18, 9.57, 9.57, 9.57, 0.00, + 980.82); + checkInst(model, 3, 3, LocalDate.of(2019, 1, 4), 199, 1, 50.00, 50.00, null, 0.99893332, 49.95, 8878.70, 9.52, 0.00, null, -9.52, + 980.82); + checkInst(model, 4, 4, LocalDate.of(2019, 1, 5), 198, 2, 50.00, 50.00, null, 0.99786779, 49.89, 8838.18, 9.48, 0.00, null, -9.48, + 980.82); + checkInst(model, 5, 5, LocalDate.of(2019, 1, 6), 197, 3, 50.00, 50.00, null, 0.99680339, 49.84, 8797.62, 9.44, 0.00, null, -9.44, + 980.82); + checkInst(model, 6, 6, LocalDate.of(2019, 1, 7), 196, 4, 50.00, 50.00, null, 0.99574012, 49.79, 8757.01, 9.39, 0.00, null, -9.39, + 980.82); + checkInst(model, 7, 7, LocalDate.of(2019, 1, 8), 195, 5, 50.00, 50.00, null, 0.99467799, 49.73, 8716.36, 9.35, 0.00, null, -9.35, + 980.82); + checkInst(model, 8, 8, LocalDate.of(2019, 1, 9), 194, 6, 50.00, 50.00, null, 0.99361699, 49.68, 8675.67, 9.31, 0.00, null, -9.31, + 980.82); + checkInst(model, 9, 9, LocalDate.of(2019, 1, 10), 193, 7, 50.00, 50.00, null, 0.99255712, 49.63, 8634.94, 9.26, 0.00, null, -9.26, + 980.82); + checkInst(model, 10, 10, LocalDate.of(2019, 1, 11), 192, 8, 50.00, 50.00, null, 0.99149839, 49.57, 8594.16, 9.22, 0.00, null, -9.22, + 980.82); + checkInst(model, 11, 11, LocalDate.of(2019, 1, 12), 191, 9, 50.00, 50.00, null, 0.99044078, 49.52, 8553.33, 9.18, 0.00, null, -9.18, + 980.82); + checkInst(model, 12, 12, LocalDate.of(2019, 1, 13), 190, 10, 50.00, 50.00, null, 0.98938430, 49.47, 8512.47, 9.13, 0.00, null, + -9.13, 980.82); + checkInst(model, 13, 13, LocalDate.of(2019, 1, 14), 189, 11, 50.00, 50.00, null, 0.98832895, 49.42, 8471.56, 9.09, 0.00, null, + -9.09, 980.82); + checkInst(model, 14, 14, LocalDate.of(2019, 1, 15), 188, 12, 50.00, 50.00, null, 0.98727472, 49.36, 8430.60, 9.05, 0.00, null, + -9.05, 980.82); + checkInst(model, 15, 15, LocalDate.of(2019, 1, 16), 187, 13, 50.00, 50.00, null, 0.98622162, 49.31, 8389.61, 9.00, 0.00, null, + -9.00, 980.82); + checkInst(model, 16, 16, LocalDate.of(2019, 1, 17), 186, 14, 50.00, 50.00, null, 0.98516964, 49.26, 8348.56, 8.96, 0.00, null, + -8.96, 980.82); + checkInst(model, 17, 17, LocalDate.of(2019, 1, 18), 185, 15, 50.00, 50.00, null, 0.98411879, 49.21, 8307.48, 8.91, 0.00, null, + -8.91, 980.82); + checkInst(model, 18, 18, LocalDate.of(2019, 1, 19), 184, 16, 50.00, 50.00, null, 0.98306905, 49.15, 8266.35, 8.87, 0.00, null, + -8.87, 980.82); + checkInst(model, 19, 19, LocalDate.of(2019, 1, 20), 183, 17, 50.00, 50.00, null, 0.98202044, 49.10, 8225.18, 8.83, 0.00, null, + -8.83, 980.82); + checkInst(model, 20, 20, LocalDate.of(2019, 1, 21), 182, 18, 50.00, 50.00, null, 0.98097294, 49.05, 8183.96, 8.78, 0.00, null, + -8.78, 980.82); + checkInst(model, 21, 21, LocalDate.of(2019, 1, 22), 181, 19, 50.00, 50.00, null, 0.97992656, 49.00, 8142.70, 8.74, 0.00, null, + -8.74, 980.82); + checkInst(model, 22, 22, LocalDate.of(2019, 1, 23), 180, 20, 50.00, 50.00, null, 0.97888129, 48.94, 8101.39, 8.69, 0.00, null, + -8.69, 980.82); + checkInst(model, 23, 23, LocalDate.of(2019, 1, 24), 179, 21, 50.00, 50.00, null, 0.97783715, 48.89, 8060.04, 8.65, 0.00, null, + -8.65, 980.82); + checkInst(model, 24, 24, LocalDate.of(2019, 1, 25), 178, 22, 50.00, 50.00, null, 0.97679411, 48.84, 8018.65, 8.61, 0.00, null, + -8.61, 980.82); + checkInst(model, 25, 25, LocalDate.of(2019, 1, 26), 177, 23, 50.00, 50.00, null, 0.97575219, 48.79, 7977.21, 8.56, 0.00, null, + -8.56, 980.82); + checkInst(model, 26, 26, LocalDate.of(2019, 1, 27), 176, 24, 50.00, 50.00, null, 0.97471138, 48.74, 7935.73, 8.52, 0.00, null, + -8.52, 980.82); + checkInst(model, 27, 27, LocalDate.of(2019, 1, 28), 175, 25, 50.00, 50.00, null, 0.97367168, 48.68, 7894.21, 8.47, 0.00, null, + -8.47, 980.82); + checkInst(model, 28, 28, LocalDate.of(2019, 1, 29), 174, 26, 50.00, 50.00, null, 0.97263309, 48.63, 7852.63, 8.43, 0.00, null, + -8.43, 980.82); + checkInst(model, 29, 29, LocalDate.of(2019, 1, 30), 173, 27, 50.00, 50.00, null, 0.97159560, 48.58, 7811.02, 8.39, 0.00, null, + -8.39, 980.82); + checkInst(model, 30, 30, LocalDate.of(2019, 1, 31), 172, 28, 50.00, 50.00, null, 0.97055922, 48.53, 7769.36, 8.34, 0.00, null, + -8.34, 980.82); + checkInst(model, 31, 31, LocalDate.of(2019, 2, 1), 171, 29, 50.00, 50.00, null, 0.96952395, 48.48, 7727.66, 8.30, 0.00, null, -8.30, + 980.82); + checkInst(model, 32, 32, LocalDate.of(2019, 2, 2), 170, 30, 50.00, 50.00, null, 0.96848979, 48.42, 7685.91, 8.25, 0.00, null, -8.25, + 980.82); + checkInst(model, 33, 33, LocalDate.of(2019, 2, 3), 169, 31, 50.00, 50.00, null, 0.96745672, 48.37, 7644.12, 8.21, 0.00, null, -8.21, + 980.82); + checkInst(model, 34, 34, LocalDate.of(2019, 2, 4), 168, 32, 50.00, 50.00, null, 0.96642476, 48.32, 7602.28, 8.16, 0.00, null, -8.16, + 980.82); + checkInst(model, 35, 35, LocalDate.of(2019, 2, 5), 167, 33, 50.00, 50.00, null, 0.96539390, 48.27, 7560.40, 8.12, 0.00, null, -8.12, + 980.82); + checkInst(model, 36, 36, LocalDate.of(2019, 2, 6), 166, 34, 50.00, 50.00, null, 0.96436413, 48.22, 7518.47, 8.07, 0.00, null, -8.07, + 980.82); + checkInst(model, 37, 37, LocalDate.of(2019, 2, 7), 165, 35, 50.00, 50.00, null, 0.96333547, 48.17, 7476.50, 8.03, 0.00, null, -8.03, + 980.82); + checkInst(model, 38, 38, LocalDate.of(2019, 2, 8), 164, 36, 50.00, 50.00, null, 0.96230790, 48.12, 7434.48, 7.98, 0.00, null, -7.98, + 980.82); + checkInst(model, 39, 39, LocalDate.of(2019, 2, 9), 163, 37, 50.00, 50.00, null, 0.96128143, 48.06, 7392.42, 7.94, 0.00, null, -7.94, + 980.82); + checkInst(model, 40, 40, LocalDate.of(2019, 2, 10), 162, 38, 50.00, 50.00, null, 0.96025606, 48.01, 7350.31, 7.89, 0.00, null, + -7.89, 980.82); + checkInst(model, 41, 41, LocalDate.of(2019, 2, 11), 161, 39, 50.00, 50.00, null, 0.95923178, 47.96, 7308.16, 7.85, 0.00, null, + -7.85, 980.82); + checkInst(model, 42, 42, LocalDate.of(2019, 2, 12), 160, 40, 50.00, 50.00, null, 0.95820859, 47.91, 7265.97, 7.80, 0.00, null, + -7.80, 980.82); + checkInst(model, 43, 43, LocalDate.of(2019, 2, 13), 159, 41, 50.00, 50.00, null, 0.95718649, 47.86, 7223.72, 7.76, 0.00, null, + -7.76, 980.82); + checkInst(model, 44, 44, LocalDate.of(2019, 2, 14), 158, 42, 50.00, 50.00, null, 0.95616548, 47.81, 7181.44, 7.71, 0.00, null, + -7.71, 980.82); + checkInst(model, 45, 45, LocalDate.of(2019, 2, 15), 157, 43, 50.00, 50.00, null, 0.95514557, 47.76, 7139.11, 7.67, 0.00, null, + -7.67, 980.82); + checkInst(model, 46, 46, LocalDate.of(2019, 2, 16), 156, 44, 50.00, 50.00, null, 0.95412674, 47.71, 7096.73, 7.62, 0.00, null, + -7.62, 980.82); + checkInst(model, 47, 47, LocalDate.of(2019, 2, 17), 155, 45, 50.00, 50.00, null, 0.95310899, 47.66, 7054.31, 7.58, 0.00, null, + -7.58, 980.82); + checkInst(model, 48, 48, LocalDate.of(2019, 2, 18), 154, 46, 50.00, 50.00, null, 0.95209233, 47.60, 7011.84, 7.53, 0.00, null, + -7.53, 980.82); + checkInst(model, 49, 49, LocalDate.of(2019, 2, 19), 153, 47, 50.00, 50.00, null, 0.95107676, 47.55, 6969.33, 7.49, 0.00, null, + -7.49, 980.82); + checkInst(model, 50, 50, LocalDate.of(2019, 2, 20), 152, 48, 50.00, 50.00, null, 0.95006227, 47.50, 6926.77, 7.44, 0.00, null, + -7.44, 980.82); + checkInst(model, 51, 51, LocalDate.of(2019, 2, 21), 151, 49, 50.00, 50.00, null, 0.94904886, 47.45, 6884.17, 7.40, 0.00, null, + -7.40, 980.82); + checkInst(model, 52, 52, LocalDate.of(2019, 2, 22), 150, 50, 50.00, 50.00, null, 0.94803653, 47.40, 6841.52, 7.35, 0.00, null, + -7.35, 980.82); + checkInst(model, 53, 53, LocalDate.of(2019, 2, 23), 149, 51, 50.00, 50.00, null, 0.94702529, 47.35, 6798.82, 7.31, 0.00, null, + -7.31, 980.82); + checkInst(model, 54, 54, LocalDate.of(2019, 2, 24), 148, 52, 50.00, 50.00, null, 0.94601512, 47.30, 6756.08, 7.26, 0.00, null, + -7.26, 980.82); + checkInst(model, 55, 55, LocalDate.of(2019, 2, 25), 147, 53, 50.00, 50.00, null, 0.94500603, 47.25, 6713.30, 7.21, 0.00, null, + -7.21, 980.82); + checkInst(model, 56, 56, LocalDate.of(2019, 2, 26), 146, 54, 50.00, 50.00, null, 0.94399801, 47.20, 6670.47, 7.17, 0.00, null, + -7.17, 980.82); + checkInst(model, 57, 57, LocalDate.of(2019, 2, 27), 145, 55, 50.00, 50.00, null, 0.94299107, 47.15, 6627.59, 7.12, 0.00, null, + -7.12, 980.82); + checkInst(model, 58, 58, LocalDate.of(2019, 2, 28), 144, 56, 50.00, 50.00, null, 0.94198521, 47.10, 6584.67, 7.08, 0.00, null, + -7.08, 980.82); + checkInst(model, 59, 59, LocalDate.of(2019, 3, 1), 143, 57, 50.00, 50.00, null, 0.94098042, 47.05, 6541.70, 7.03, 0.00, null, -7.03, + 980.82); + checkInst(model, 60, 60, LocalDate.of(2019, 3, 2), 142, 58, 50.00, 50.00, null, 0.93997669, 47.00, 6498.68, 6.99, 0.00, null, -6.99, + 980.82); + checkInst(model, 61, 61, LocalDate.of(2019, 3, 3), 141, 59, 50.00, 50.00, null, 0.93897404, 46.95, 6455.62, 6.94, 0.00, null, -6.94, + 980.82); + checkInst(model, 62, 62, LocalDate.of(2019, 3, 4), 140, 60, 50.00, 50.00, null, 0.93797246, 46.90, 6412.51, 6.89, 0.00, null, -6.89, + 980.82); + checkInst(model, 63, 63, LocalDate.of(2019, 3, 5), 139, 61, 50.00, 50.00, null, 0.93697195, 46.85, 6369.36, 6.85, 0.00, null, -6.85, + 980.82); + checkInst(model, 64, 64, LocalDate.of(2019, 3, 6), 138, 62, 50.00, 50.00, null, 0.93597251, 46.80, 6326.16, 6.80, 0.00, null, -6.80, + 980.82); + checkInst(model, 65, 65, LocalDate.of(2019, 3, 7), 137, 63, 50.00, 50.00, null, 0.93497413, 46.75, 6282.92, 6.76, 0.00, null, -6.76, + 980.82); + checkInst(model, 66, 66, LocalDate.of(2019, 3, 8), 136, 64, 50.00, 50.00, null, 0.93397681, 46.70, 6239.63, 6.71, 0.00, null, -6.71, + 980.82); + checkInst(model, 67, 67, LocalDate.of(2019, 3, 9), 135, 65, 50.00, 50.00, null, 0.93298056, 46.65, 6196.29, 6.66, 0.00, null, -6.66, + 980.82); + checkInst(model, 68, 68, LocalDate.of(2019, 3, 10), 134, 66, 50.00, 50.00, null, 0.93198538, 46.60, 6152.91, 6.62, 0.00, null, + -6.62, 980.82); + checkInst(model, 69, 69, LocalDate.of(2019, 3, 11), 133, 67, 50.00, 50.00, null, 0.93099125, 46.55, 6109.48, 6.57, 0.00, null, + -6.57, 980.82); + checkInst(model, 70, 70, LocalDate.of(2019, 3, 12), 132, 68, 50.00, 50.00, null, 0.92999818, 46.50, 6066.00, 6.52, 0.00, null, + -6.52, 980.82); + checkInst(model, 71, 71, LocalDate.of(2019, 3, 13), 131, 69, 50.00, 50.00, null, 0.92900618, 46.45, 6022.48, 6.48, 0.00, null, + -6.48, 980.82); + checkInst(model, 72, 72, LocalDate.of(2019, 3, 14), 130, 70, 50.00, 50.00, null, 0.92801523, 46.40, 5978.91, 6.43, 0.00, null, + -6.43, 980.82); + checkInst(model, 73, 73, LocalDate.of(2019, 3, 15), 129, 71, 50.00, 50.00, null, 0.92702534, 46.35, 5935.29, 6.38, 0.00, null, + -6.38, 980.82); + checkInst(model, 74, 74, LocalDate.of(2019, 3, 16), 128, 72, 50.00, 50.00, null, 0.92603650, 46.30, 5891.63, 6.34, 0.00, null, + -6.34, 980.82); + checkInst(model, 75, 75, LocalDate.of(2019, 3, 17), 127, 73, 50.00, 50.00, null, 0.92504872, 46.25, 5847.92, 6.29, 0.00, null, + -6.29, 980.82); + checkInst(model, 76, 76, LocalDate.of(2019, 3, 18), 126, 74, 50.00, 50.00, null, 0.92406200, 46.20, 5804.17, 6.24, 0.00, null, + -6.24, 980.82); + checkInst(model, 77, 77, LocalDate.of(2019, 3, 19), 125, 75, 50.00, 50.00, null, 0.92307632, 46.15, 5760.36, 6.20, 0.00, null, + -6.20, 980.82); + checkInst(model, 78, 78, LocalDate.of(2019, 3, 20), 124, 76, 50.00, 50.00, null, 0.92209170, 46.10, 5716.52, 6.15, 0.00, null, + -6.15, 980.82); + checkInst(model, 79, 79, LocalDate.of(2019, 3, 21), 123, 77, 50.00, 50.00, null, 0.92110813, 46.06, 5672.62, 6.10, 0.00, null, + -6.10, 980.82); + checkInst(model, 80, 80, LocalDate.of(2019, 3, 22), 122, 78, 50.00, 50.00, null, 0.92012560, 46.01, 5628.68, 6.06, 0.00, null, + -6.06, 980.82); + checkInst(model, 81, 81, LocalDate.of(2019, 3, 23), 121, 79, 50.00, 50.00, null, 0.91914413, 45.96, 5584.69, 6.01, 0.00, null, + -6.01, 980.82); + checkInst(model, 82, 82, LocalDate.of(2019, 3, 24), 120, 80, 50.00, 50.00, null, 0.91816370, 45.91, 5540.65, 5.96, 0.00, null, + -5.96, 980.82); + checkInst(model, 83, 83, LocalDate.of(2019, 3, 25), 119, 81, 50.00, 50.00, null, 0.91718432, 45.86, 5496.57, 5.92, 0.00, null, + -5.92, 980.82); + checkInst(model, 84, 84, LocalDate.of(2019, 3, 26), 118, 82, 50.00, 50.00, null, 0.91620598, 45.81, 5452.44, 5.87, 0.00, null, + -5.87, 980.82); + checkInst(model, 85, 85, LocalDate.of(2019, 3, 27), 117, 83, 50.00, 50.00, null, 0.91522868, 45.76, 5408.26, 5.82, 0.00, null, + -5.82, 980.82); + checkInst(model, 86, 86, LocalDate.of(2019, 3, 28), 116, 84, 50.00, 50.00, null, 0.91425243, 45.71, 5364.03, 5.78, 0.00, null, + -5.78, 980.82); + checkInst(model, 87, 87, LocalDate.of(2019, 3, 29), 115, 85, 50.00, 50.00, null, 0.91327722, 45.66, 5319.76, 5.73, 0.00, null, + -5.73, 980.82); + checkInst(model, 88, 88, LocalDate.of(2019, 3, 30), 114, 86, 50.00, 50.00, null, 0.91230305, 45.62, 5275.44, 5.68, 0.00, null, + -5.68, 980.82); + checkInst(model, 89, 89, LocalDate.of(2019, 3, 31), 113, 87, 50.00, 50.00, null, 0.91132992, 45.57, 5231.08, 5.63, 0.00, null, + -5.63, 980.82); + checkInst(model, 90, 90, LocalDate.of(2019, 4, 1), 112, 88, 50.00, 50.00, null, 0.91035783, 45.52, 5186.66, 5.59, 0.00, null, -5.59, + 980.82); + checkInst(model, 91, 91, LocalDate.of(2019, 4, 2), 111, 89, 50.00, 50.00, null, 0.90938677, 45.47, 5142.20, 5.54, 0.00, null, -5.54, + 980.82); + checkInst(model, 92, 92, LocalDate.of(2019, 4, 3), 110, 90, 50.00, 50.00, null, 0.90841675, 45.42, 5097.69, 5.49, 0.00, null, -5.49, + 980.82); + checkInst(model, 93, 93, LocalDate.of(2019, 4, 4), 109, 91, 50.00, 50.00, null, 0.90744776, 45.37, 5053.13, 5.44, 0.00, null, -5.44, + 980.82); + checkInst(model, 94, 94, LocalDate.of(2019, 4, 5), 108, 92, 50.00, 50.00, null, 0.90647981, 45.32, 5008.53, 5.40, 0.00, null, -5.40, + 980.82); + checkInst(model, 95, 95, LocalDate.of(2019, 4, 6), 107, 93, 50.00, 50.00, null, 0.90551289, 45.28, 4963.88, 5.35, 0.00, null, -5.35, + 980.82); + checkInst(model, 96, 96, LocalDate.of(2019, 4, 7), 106, 94, 50.00, 50.00, null, 0.90454700, 45.23, 4919.18, 5.30, 0.00, null, -5.30, + 980.82); + checkInst(model, 97, 97, LocalDate.of(2019, 4, 8), 105, 95, 50.00, 50.00, null, 0.90358215, 45.18, 4874.43, 5.25, 0.00, null, -5.25, + 980.82); + checkInst(model, 98, 98, LocalDate.of(2019, 4, 9), 104, 96, 50.00, 50.00, null, 0.90261832, 45.13, 4829.64, 5.20, 0.00, null, -5.20, + 980.82); + checkInst(model, 99, 99, LocalDate.of(2019, 4, 10), 103, 97, 50.00, 50.00, null, 0.90165552, 45.08, 4784.79, 5.16, 0.00, null, + -5.16, 980.82); + checkInst(model, 100, 100, LocalDate.of(2019, 4, 11), 102, 98, 50.00, 50.00, null, 0.90069374, 45.03, 4739.90, 5.11, 0.00, null, + -5.11, 980.82); + checkInst(model, 101, 101, LocalDate.of(2019, 4, 12), 101, 99, 50.00, 50.00, null, 0.89973299, 44.99, 4694.96, 5.06, 0.00, null, + -5.06, 980.82); + checkInst(model, 102, 102, LocalDate.of(2019, 4, 13), 100, 100, 50.00, 50.00, null, 0.89877327, 44.94, 4649.98, 5.01, 0.00, null, + -5.01, 980.82); + checkInst(model, 103, 103, LocalDate.of(2019, 4, 14), 99, 101, 50.00, 50.00, null, 0.89781457, 44.89, 4604.94, 4.97, 0.00, null, + -4.97, 980.82); + checkInst(model, 104, 104, LocalDate.of(2019, 4, 15), 98, 102, 50.00, 50.00, null, 0.89685689, 44.84, 4559.86, 4.92, 0.00, null, + -4.92, 980.82); + checkInst(model, 105, 105, LocalDate.of(2019, 4, 16), 97, 103, 50.00, 50.00, null, 0.89590024, 44.80, 4514.73, 4.87, 0.00, null, + -4.87, 980.82); + checkInst(model, 106, 106, LocalDate.of(2019, 4, 17), 96, 104, 50.00, 50.00, null, 0.89494460, 44.75, 4469.55, 4.82, 0.00, null, + -4.82, 980.82); + checkInst(model, 107, 107, LocalDate.of(2019, 4, 18), 95, 105, 50.00, 50.00, null, 0.89398999, 44.70, 4424.32, 4.77, 0.00, null, + -4.77, 980.82); + checkInst(model, 108, 108, LocalDate.of(2019, 4, 19), 94, 106, 50.00, 50.00, null, 0.89303639, 44.65, 4379.05, 4.72, 0.00, null, + -4.72, 980.82); + checkInst(model, 109, 109, LocalDate.of(2019, 4, 20), 93, 107, 50.00, 50.00, null, 0.89208381, 44.60, 4333.72, 4.68, 0.00, null, + -4.68, 980.82); + checkInst(model, 110, 110, LocalDate.of(2019, 4, 21), 92, 108, 50.00, 50.00, null, 0.89113225, 44.56, 4288.35, 4.63, 0.00, null, + -4.63, 980.82); + checkInst(model, 111, 111, LocalDate.of(2019, 4, 22), 91, 109, 50.00, 50.00, null, 0.89018170, 44.51, 4242.93, 4.58, 0.00, null, + -4.58, 980.82); + checkInst(model, 112, 112, LocalDate.of(2019, 4, 23), 90, 110, 50.00, 50.00, null, 0.88923216, 44.46, 4197.46, 4.53, 0.00, null, + -4.53, 980.82); + checkInst(model, 113, 113, LocalDate.of(2019, 4, 24), 89, 111, 50.00, 50.00, null, 0.88828364, 44.41, 4151.94, 4.48, 0.00, null, + -4.48, 980.82); + checkInst(model, 114, 114, LocalDate.of(2019, 4, 25), 88, 112, 50.00, 50.00, null, 0.88733613, 44.37, 4106.38, 4.43, 0.00, null, + -4.43, 980.82); + checkInst(model, 115, 115, LocalDate.of(2019, 4, 26), 87, 113, 50.00, 50.00, null, 0.88638963, 44.32, 4060.76, 4.38, 0.00, null, + -4.38, 980.82); + checkInst(model, 116, 116, LocalDate.of(2019, 4, 27), 86, 114, 50.00, 50.00, null, 0.88544414, 44.27, 4015.10, 4.34, 0.00, null, + -4.34, 980.82); + checkInst(model, 117, 117, LocalDate.of(2019, 4, 28), 85, 115, 50.00, 50.00, null, 0.88449966, 44.22, 3969.38, 4.29, 0.00, null, + -4.29, 980.82); + checkInst(model, 118, 118, LocalDate.of(2019, 4, 29), 84, 116, 50.00, 50.00, null, 0.88355619, 44.18, 3923.62, 4.24, 0.00, null, + -4.24, 980.82); + checkInst(model, 119, 119, LocalDate.of(2019, 4, 30), 83, 117, 50.00, 50.00, null, 0.88261372, 44.13, 3877.81, 4.19, 0.00, null, + -4.19, 980.82); + checkInst(model, 120, 120, LocalDate.of(2019, 5, 1), 82, 118, 50.00, 50.00, null, 0.88167226, 44.08, 3831.95, 4.14, 0.00, null, + -4.14, 980.82); + checkInst(model, 121, 121, LocalDate.of(2019, 5, 2), 81, 119, 50.00, 50.00, null, 0.88073180, 44.04, 3786.04, 4.09, 0.00, null, + -4.09, 980.82); + checkInst(model, 122, 122, LocalDate.of(2019, 5, 3), 80, 120, 50.00, 50.00, null, 0.87979234, 43.99, 3740.09, 4.04, 0.00, null, + -4.04, 980.82); + checkInst(model, 123, 123, LocalDate.of(2019, 5, 4), 79, 121, 50.00, 50.00, null, 0.87885389, 43.94, 3694.08, 3.99, 0.00, null, + -3.99, 980.82); + checkInst(model, 124, 124, LocalDate.of(2019, 5, 5), 78, 122, 50.00, 50.00, null, 0.87791644, 43.90, 3648.03, 3.94, 0.00, null, + -3.94, 980.82); + checkInst(model, 125, 125, LocalDate.of(2019, 5, 6), 77, 123, 50.00, 50.00, null, 0.87697999, 43.85, 3601.92, 3.90, 0.00, null, + -3.90, 980.82); + checkInst(model, 126, 126, LocalDate.of(2019, 5, 7), 76, 124, 50.00, 50.00, null, 0.87604453, 43.80, 3555.77, 3.85, 0.00, null, + -3.85, 980.82); + checkInst(model, 127, 127, LocalDate.of(2019, 5, 8), 75, 125, 50.00, 50.00, null, 0.87511008, 43.76, 3509.56, 3.80, 0.00, null, + -3.80, 980.82); + checkInst(model, 128, 128, LocalDate.of(2019, 5, 9), 74, 126, 50.00, 50.00, null, 0.87417662, 43.71, 3463.31, 3.75, 0.00, null, + -3.75, 980.82); + checkInst(model, 129, 129, LocalDate.of(2019, 5, 10), 73, 127, 50.00, 50.00, null, 0.87324416, 43.66, 3417.01, 3.70, 0.00, null, + -3.70, 980.82); + checkInst(model, 130, 130, LocalDate.of(2019, 5, 11), 72, 128, 50.00, 50.00, null, 0.87231269, 43.62, 3370.66, 3.65, 0.00, null, + -3.65, 980.82); + checkInst(model, 131, 131, LocalDate.of(2019, 5, 12), 71, 129, 50.00, 50.00, null, 0.87138221, 43.57, 3324.26, 3.60, 0.00, null, + -3.60, 980.82); + checkInst(model, 132, 132, LocalDate.of(2019, 5, 13), 70, 130, 50.00, 50.00, null, 0.87045273, 43.52, 3277.81, 3.55, 0.00, null, + -3.55, 980.82); + checkInst(model, 133, 133, LocalDate.of(2019, 5, 14), 69, 131, 50.00, 50.00, null, 0.86952424, 43.48, 3231.31, 3.50, 0.00, null, + -3.50, 980.82); + checkInst(model, 134, 134, LocalDate.of(2019, 5, 15), 68, 132, 50.00, 50.00, null, 0.86859674, 43.43, 3184.76, 3.45, 0.00, null, + -3.45, 980.82); + checkInst(model, 135, 135, LocalDate.of(2019, 5, 16), 67, 133, 50.00, 50.00, null, 0.86767023, 43.38, 3138.16, 3.40, 0.00, null, + -3.40, 980.82); + checkInst(model, 136, 136, LocalDate.of(2019, 5, 17), 66, 134, 50.00, 50.00, null, 0.86674471, 43.34, 3091.51, 3.35, 0.00, null, + -3.35, 980.82); + checkInst(model, 137, 137, LocalDate.of(2019, 5, 18), 65, 135, 50.00, 50.00, null, 0.86582017, 43.29, 3044.81, 3.30, 0.00, null, + -3.30, 980.82); + checkInst(model, 138, 138, LocalDate.of(2019, 5, 19), 64, 136, 50.00, 50.00, null, 0.86489662, 43.24, 2998.06, 3.25, 0.00, null, + -3.25, 980.82); + checkInst(model, 139, 139, LocalDate.of(2019, 5, 20), 63, 137, 50.00, 50.00, null, 0.86397406, 43.20, 2951.26, 3.20, 0.00, null, + -3.20, 980.82); + checkInst(model, 140, 140, LocalDate.of(2019, 5, 21), 62, 138, 50.00, 50.00, null, 0.86305248, 43.15, 2904.42, 3.15, 0.00, null, + -3.15, 980.82); + checkInst(model, 141, 141, LocalDate.of(2019, 5, 22), 61, 139, 50.00, 50.00, null, 0.86213188, 43.11, 2857.52, 3.10, 0.00, null, + -3.10, 980.82); + checkInst(model, 142, 142, LocalDate.of(2019, 5, 23), 60, 140, 50.00, 50.00, null, 0.86121227, 43.06, 2810.57, 3.05, 0.00, null, + -3.05, 980.82); + checkInst(model, 143, 143, LocalDate.of(2019, 5, 24), 59, 141, 50.00, 50.00, null, 0.86029363, 43.01, 2763.57, 3.00, 0.00, null, + -3.00, 980.82); + checkInst(model, 144, 144, LocalDate.of(2019, 5, 25), 58, 142, 50.00, 50.00, null, 0.85937598, 42.97, 2716.52, 2.95, 0.00, null, + -2.95, 980.82); + checkInst(model, 145, 145, LocalDate.of(2019, 5, 26), 57, 143, 50.00, 50.00, null, 0.85845930, 42.92, 2669.42, 2.90, 0.00, null, + -2.90, 980.82); + checkInst(model, 146, 146, LocalDate.of(2019, 5, 27), 56, 144, 50.00, 50.00, null, 0.85754361, 42.88, 2622.27, 2.85, 0.00, null, + -2.85, 980.82); + checkInst(model, 147, 147, LocalDate.of(2019, 5, 28), 55, 145, 50.00, 50.00, null, 0.85662889, 42.83, 2575.07, 2.80, 0.00, null, + -2.80, 980.82); + checkInst(model, 148, 148, LocalDate.of(2019, 5, 29), 54, 146, 50.00, 50.00, null, 0.85571514, 42.79, 2527.82, 2.75, 0.00, null, + -2.75, 980.82); + checkInst(model, 149, 149, LocalDate.of(2019, 5, 30), 53, 147, 50.00, 50.00, null, 0.85480237, 42.74, 2480.52, 2.70, 0.00, null, + -2.70, 980.82); + checkInst(model, 150, 150, LocalDate.of(2019, 5, 31), 52, 148, 50.00, 50.00, null, 0.85389057, 42.69, 2433.17, 2.65, 0.00, null, + -2.65, 980.82); + checkInst(model, 151, 151, LocalDate.of(2019, 6, 1), 51, 149, 50.00, 50.00, null, 0.85297975, 42.65, 2385.77, 2.60, 0.00, null, + -2.60, 980.82); + checkInst(model, 152, 152, LocalDate.of(2019, 6, 2), 50, 150, 50.00, 50.00, null, 0.85206990, 42.60, 2338.31, 2.55, 0.00, null, + -2.55, 980.82); + checkInst(model, 153, 153, LocalDate.of(2019, 6, 3), 49, 151, 50.00, 50.00, null, 0.85116101, 42.56, 2290.81, 2.50, 0.00, null, + -2.50, 980.82); + checkInst(model, 154, 154, LocalDate.of(2019, 6, 4), 48, 152, 50.00, 50.00, null, 0.85025310, 42.51, 2243.26, 2.45, 0.00, null, + -2.45, 980.82); + checkInst(model, 155, 155, LocalDate.of(2019, 6, 5), 47, 153, 50.00, 50.00, null, 0.84934616, 42.47, 2195.65, 2.40, 0.00, null, + -2.40, 980.82); + checkInst(model, 156, 156, LocalDate.of(2019, 6, 6), 46, 154, 50.00, 50.00, null, 0.84844018, 42.42, 2148.00, 2.34, 0.00, null, + -2.34, 980.82); + checkInst(model, 157, 157, LocalDate.of(2019, 6, 7), 45, 155, 50.00, 50.00, null, 0.84753517, 42.38, 2100.29, 2.29, 0.00, null, + -2.29, 980.82); + checkInst(model, 158, 158, LocalDate.of(2019, 6, 8), 44, 156, 50.00, 50.00, null, 0.84663113, 42.33, 2052.53, 2.24, 0.00, null, + -2.24, 980.82); + checkInst(model, 159, 159, LocalDate.of(2019, 6, 9), 43, 157, 50.00, 50.00, null, 0.84572805, 42.29, 2004.73, 2.19, 0.00, null, + -2.19, 980.82); + checkInst(model, 160, 160, LocalDate.of(2019, 6, 10), 42, 158, 50.00, 50.00, null, 0.84482593, 42.24, 1956.87, 2.14, 0.00, null, + -2.14, 980.82); + checkInst(model, 161, 161, LocalDate.of(2019, 6, 11), 41, 159, 50.00, 50.00, null, 0.84392477, 42.20, 1908.96, 2.09, 0.00, null, + -2.09, 980.82); + checkInst(model, 162, 162, LocalDate.of(2019, 6, 12), 40, 160, 50.00, 50.00, null, 0.84302458, 42.15, 1860.99, 2.04, 0.00, null, + -2.04, 980.82); + checkInst(model, 163, 163, LocalDate.of(2019, 6, 13), 39, 161, 50.00, 50.00, null, 0.84212535, 42.11, 1812.98, 1.99, 0.00, null, + -1.99, 980.82); + checkInst(model, 164, 164, LocalDate.of(2019, 6, 14), 38, 162, 50.00, 50.00, null, 0.84122707, 42.06, 1764.92, 1.94, 0.00, null, + -1.94, 980.82); + checkInst(model, 165, 165, LocalDate.of(2019, 6, 15), 37, 163, 50.00, 50.00, null, 0.84032975, 42.02, 1716.80, 1.88, 0.00, null, + -1.88, 980.82); + checkInst(model, 166, 166, LocalDate.of(2019, 6, 16), 36, 164, 50.00, 50.00, null, 0.83943340, 41.97, 1668.64, 1.83, 0.00, null, + -1.83, 980.82); + checkInst(model, 167, 167, LocalDate.of(2019, 6, 17), 35, 165, 50.00, 50.00, null, 0.83853799, 41.93, 1620.42, 1.78, 0.00, null, + -1.78, 980.82); + checkInst(model, 168, 168, LocalDate.of(2019, 6, 18), 34, 166, 50.00, 50.00, null, 0.83764354, 41.88, 1572.15, 1.73, 0.00, null, + -1.73, 980.82); + checkInst(model, 169, 169, LocalDate.of(2019, 6, 19), 33, 167, 50.00, 50.00, null, 0.83675005, 41.84, 1523.83, 1.68, 0.00, null, + -1.68, 980.82); + checkInst(model, 170, 170, LocalDate.of(2019, 6, 20), 32, 168, 50.00, 50.00, null, 0.83585751, 41.79, 1475.45, 1.63, 0.00, null, + -1.63, 980.82); + checkInst(model, 171, 171, LocalDate.of(2019, 6, 21), 31, 169, 50.00, 50.00, null, 0.83496592, 41.75, 1427.03, 1.58, 0.00, null, + -1.58, 980.82); + checkInst(model, 172, 172, LocalDate.of(2019, 6, 22), 30, 170, 50.00, 50.00, null, 0.83407528, 41.70, 1378.55, 1.52, 0.00, null, + -1.52, 980.82); + checkInst(model, 173, 173, LocalDate.of(2019, 6, 23), 29, 171, 50.00, 50.00, null, 0.83318560, 41.66, 1330.02, 1.47, 0.00, null, + -1.47, 980.82); + checkInst(model, 174, 174, LocalDate.of(2019, 6, 24), 28, 172, 50.00, 50.00, null, 0.83229686, 41.61, 1281.45, 1.42, 0.00, null, + -1.42, 980.82); + checkInst(model, 175, 175, LocalDate.of(2019, 6, 25), 27, 173, 50.00, 50.00, null, 0.83140907, 41.57, 1232.81, 1.37, 0.00, null, + -1.37, 980.82); + checkInst(model, 176, 176, LocalDate.of(2019, 6, 26), 26, 174, 50.00, 50.00, null, 0.83052222, 41.53, 1184.13, 1.32, 0.00, null, + -1.32, 980.82); + checkInst(model, 177, 177, LocalDate.of(2019, 6, 27), 25, 175, 50.00, 50.00, null, 0.82963633, 41.48, 1135.39, 1.26, 0.00, null, + -1.26, 980.82); + checkInst(model, 178, 178, LocalDate.of(2019, 6, 28), 24, 176, 50.00, 50.00, null, 0.82875137, 41.44, 1086.61, 1.21, 0.00, null, + -1.21, 980.82); + checkInst(model, 179, 179, LocalDate.of(2019, 6, 29), 23, 177, 50.00, 50.00, null, 0.82786736, 41.39, 1037.77, 1.16, 0.00, null, + -1.16, 980.82); + checkInst(model, 180, 180, LocalDate.of(2019, 6, 30), 22, 178, 50.00, 50.00, null, 0.82698430, 41.35, 988.88, 1.11, 0.00, null, + -1.11, 980.82); + checkInst(model, 181, 181, LocalDate.of(2019, 7, 1), 21, 179, 50.00, 50.00, null, 0.82610217, 41.31, 939.93, 1.06, 0.00, null, + -1.06, 980.82); + checkInst(model, 182, 182, LocalDate.of(2019, 7, 2), 20, 180, 50.00, 50.00, null, 0.82522099, 41.26, 890.93, 1.00, 0.00, null, + -1.00, 980.82); + checkInst(model, 183, 183, LocalDate.of(2019, 7, 3), 19, 181, 50.00, 50.00, null, 0.82434075, 41.22, 841.89, 0.95, 0.00, null, + -0.95, 980.82); + checkInst(model, 184, 184, LocalDate.of(2019, 7, 4), 18, 182, 50.00, 50.00, null, 0.82346144, 41.17, 792.79, 0.90, 0.00, null, + -0.90, 980.82); + checkInst(model, 185, 185, LocalDate.of(2019, 7, 5), 17, 183, 50.00, 50.00, null, 0.82258308, 41.13, 743.63, 0.85, 0.00, null, + -0.85, 980.82); + checkInst(model, 186, 186, LocalDate.of(2019, 7, 6), 16, 184, 50.00, 50.00, null, 0.82170565, 41.09, 694.43, 0.79, 0.00, null, + -0.79, 980.82); + checkInst(model, 187, 187, LocalDate.of(2019, 7, 7), 15, 185, 50.00, 50.00, null, 0.82082916, 41.04, 645.17, 0.74, 0.00, null, + -0.74, 980.82); + checkInst(model, 188, 188, LocalDate.of(2019, 7, 8), 14, 186, 50.00, 50.00, null, 0.81995360, 41.00, 595.86, 0.69, 0.00, null, + -0.69, 980.82); + checkInst(model, 189, 189, LocalDate.of(2019, 7, 9), 13, 187, 50.00, 50.00, null, 0.81907897, 40.95, 546.49, 0.64, 0.00, null, + -0.64, 980.82); + checkInst(model, 190, 190, LocalDate.of(2019, 7, 10), 12, 188, 50.00, 50.00, null, 0.81820528, 40.91, 497.08, 0.58, 0.00, null, + -0.58, 980.82); + checkInst(model, 191, 191, LocalDate.of(2019, 7, 11), 11, 189, 50.00, 50.00, null, 0.81733252, 40.87, 447.61, 0.53, 0.00, null, + -0.53, 980.82); + checkInst(model, 192, 192, LocalDate.of(2019, 7, 12), 10, 190, 50.00, 50.00, null, 0.81646069, 40.82, 398.08, 0.48, 0.00, null, + -0.48, 980.82); + checkInst(model, 193, 193, LocalDate.of(2019, 7, 13), 9, 191, 50.00, 50.00, null, 0.81558979, 40.78, 348.51, 0.43, 0.00, null, + -0.43, 980.82); + checkInst(model, 194, 194, LocalDate.of(2019, 7, 14), 8, 192, 50.00, 50.00, null, 0.81471983, 40.74, 298.88, 0.37, 0.00, null, + -0.37, 980.82); + checkInst(model, 195, 195, LocalDate.of(2019, 7, 15), 7, 193, 50.00, 50.00, null, 0.81385078, 40.69, 249.20, 0.32, 0.00, null, + -0.32, 980.82); + checkInst(model, 196, 196, LocalDate.of(2019, 7, 16), 6, 194, 50.00, 50.00, null, 0.81298267, 40.65, 199.47, 0.27, 0.00, null, + -0.27, 980.82); + checkInst(model, 197, 197, LocalDate.of(2019, 7, 17), 5, 195, 50.00, 50.00, null, 0.81211548, 40.61, 149.68, 0.21, 0.00, null, + -0.21, 980.82); + checkInst(model, 198, 198, LocalDate.of(2019, 7, 18), 4, 196, 50.00, 50.00, null, 0.81124922, 40.56, 99.84, 0.16, 0.00, null, -0.16, + 980.82); + checkInst(model, 199, 199, LocalDate.of(2019, 7, 19), 3, 197, 50.00, 50.00, null, 0.81038388, 40.52, 49.95, 0.11, 0.00, null, -0.11, + 980.82); + checkInst(model, 200, 200, LocalDate.of(2019, 7, 20), 2, 198, 50.00, 50.00, null, 0.80951946, 40.48, 0.00, 0.05, 0.00, null, -0.05, + 980.82); + + } + + @Test + void testExcessPayment_term200_originationFee1000_netDisbursement9000_pay70_80() { + final ProjectedAmortizationScheduleModel model = generateModel(); + + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(1), new BigDecimal("70")); + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(2), new BigDecimal("80")); + + checkInst(model, 0, 0, EXPECTED_DISBURSEMENT_DATE, 202, 0, -9000.00, null, null, 1.00000000, -9000.00, 9000.00, null, null, null, + null, 1000.00); + + checkInst(model, 1, 1, LocalDate.of(2019, 1, 2), 201, 0, 50.00, 50.00, 70.00, 1.00000000, 70.00, 8959.61, 9.61, 28.70, 13.44, 3.83, + 986.56); + checkInst(model, 2, 2, LocalDate.of(2019, 1, 3), 200, 0, 50.00, 50.00, 80.00, 1.00000000, 80.00, 8919.18, 9.57, 15.26, 15.26, 5.70, + 971.30); + + checkInst(model, 3, 3, LocalDate.of(2019, 1, 4), 199, 1, 50.00, 50.00, null, 0.99893332, 49.95, 8878.70, 9.52, 0.00, null, -9.52, + 971.30); + checkInst(model, 4, 4, LocalDate.of(2019, 1, 5), 198, 2, 50.00, 50.00, null, 0.99786779, 49.89, 8838.18, 9.48, 0.00, null, -9.48, + 971.30); + checkInst(model, 5, 5, LocalDate.of(2019, 1, 6), 197, 3, 50.00, 50.00, null, 0.99680339, 49.84, 8797.62, 9.44, 0.00, null, -9.44, + 971.30); + checkInst(model, 6, 6, LocalDate.of(2019, 1, 7), 196, 4, 50.00, 50.00, null, 0.99574012, 49.79, 8757.01, 9.39, 0.00, null, -9.39, + 971.30); + checkInst(model, 7, 7, LocalDate.of(2019, 1, 8), 195, 5, 50.00, 50.00, null, 0.99467799, 49.73, 8716.36, 9.35, 0.00, null, -9.35, + 971.30); + checkInst(model, 8, 8, LocalDate.of(2019, 1, 9), 194, 6, 50.00, 50.00, null, 0.99361699, 49.68, 8675.67, 9.31, 0.00, null, -9.31, + 971.30); + checkInst(model, 9, 9, LocalDate.of(2019, 1, 10), 193, 7, 50.00, 50.00, null, 0.99255712, 49.63, 8634.94, 9.26, 0.00, null, -9.26, + 971.30); + checkInst(model, 10, 10, LocalDate.of(2019, 1, 11), 192, 8, 50.00, 50.00, null, 0.99149839, 49.57, 8594.16, 9.22, 0.00, null, -9.22, + 971.30); + checkInst(model, 11, 11, LocalDate.of(2019, 1, 12), 191, 9, 50.00, 50.00, null, 0.99044078, 49.52, 8553.33, 9.18, 0.00, null, -9.18, + 971.30); + checkInst(model, 12, 12, LocalDate.of(2019, 1, 13), 190, 10, 50.00, 50.00, null, 0.98938430, 49.47, 8512.47, 9.13, 0.00, null, + -9.13, 971.30); + checkInst(model, 13, 13, LocalDate.of(2019, 1, 14), 189, 11, 50.00, 50.00, null, 0.98832895, 49.42, 8471.56, 9.09, 0.00, null, + -9.09, 971.30); + checkInst(model, 14, 14, LocalDate.of(2019, 1, 15), 188, 12, 50.00, 50.00, null, 0.98727472, 49.36, 8430.60, 9.05, 0.00, null, + -9.05, 971.30); + checkInst(model, 15, 15, LocalDate.of(2019, 1, 16), 187, 13, 50.00, 50.00, null, 0.98622162, 49.31, 8389.61, 9.00, 0.00, null, + -9.00, 971.30); + checkInst(model, 16, 16, LocalDate.of(2019, 1, 17), 186, 14, 50.00, 50.00, null, 0.98516964, 49.26, 8348.56, 8.96, 0.00, null, + -8.96, 971.30); + checkInst(model, 17, 17, LocalDate.of(2019, 1, 18), 185, 15, 50.00, 50.00, null, 0.98411879, 49.21, 8307.48, 8.91, 0.00, null, + -8.91, 971.30); + checkInst(model, 18, 18, LocalDate.of(2019, 1, 19), 184, 16, 50.00, 50.00, null, 0.98306905, 49.15, 8266.35, 8.87, 0.00, null, + -8.87, 971.30); + checkInst(model, 19, 19, LocalDate.of(2019, 1, 20), 183, 17, 50.00, 50.00, null, 0.98202044, 49.10, 8225.18, 8.83, 0.00, null, + -8.83, 971.30); + checkInst(model, 20, 20, LocalDate.of(2019, 1, 21), 182, 18, 50.00, 50.00, null, 0.98097294, 49.05, 8183.96, 8.78, 0.00, null, + -8.78, 971.30); + checkInst(model, 21, 21, LocalDate.of(2019, 1, 22), 181, 19, 50.00, 50.00, null, 0.97992656, 49.00, 8142.70, 8.74, 0.00, null, + -8.74, 971.30); + checkInst(model, 22, 22, LocalDate.of(2019, 1, 23), 180, 20, 50.00, 50.00, null, 0.97888129, 48.94, 8101.39, 8.69, 0.00, null, + -8.69, 971.30); + checkInst(model, 23, 23, LocalDate.of(2019, 1, 24), 179, 21, 50.00, 50.00, null, 0.97783715, 48.89, 8060.04, 8.65, 0.00, null, + -8.65, 971.30); + checkInst(model, 24, 24, LocalDate.of(2019, 1, 25), 178, 22, 50.00, 50.00, null, 0.97679411, 48.84, 8018.65, 8.61, 0.00, null, + -8.61, 971.30); + checkInst(model, 25, 25, LocalDate.of(2019, 1, 26), 177, 23, 50.00, 50.00, null, 0.97575219, 48.79, 7977.21, 8.56, 0.00, null, + -8.56, 971.30); + checkInst(model, 26, 26, LocalDate.of(2019, 1, 27), 176, 24, 50.00, 50.00, null, 0.97471138, 48.74, 7935.73, 8.52, 0.00, null, + -8.52, 971.30); + checkInst(model, 27, 27, LocalDate.of(2019, 1, 28), 175, 25, 50.00, 50.00, null, 0.97367168, 48.68, 7894.21, 8.47, 0.00, null, + -8.47, 971.30); + checkInst(model, 28, 28, LocalDate.of(2019, 1, 29), 174, 26, 50.00, 50.00, null, 0.97263309, 48.63, 7852.63, 8.43, 0.00, null, + -8.43, 971.30); + checkInst(model, 29, 29, LocalDate.of(2019, 1, 30), 173, 27, 50.00, 50.00, null, 0.97159560, 48.58, 7811.02, 8.39, 0.00, null, + -8.39, 971.30); + checkInst(model, 30, 30, LocalDate.of(2019, 1, 31), 172, 28, 50.00, 50.00, null, 0.97055922, 48.53, 7769.36, 8.34, 0.00, null, + -8.34, 971.30); + checkInst(model, 31, 31, LocalDate.of(2019, 2, 1), 171, 29, 50.00, 50.00, null, 0.96952395, 48.48, 7727.66, 8.30, 0.00, null, -8.30, + 971.30); + checkInst(model, 32, 32, LocalDate.of(2019, 2, 2), 170, 30, 50.00, 50.00, null, 0.96848979, 48.42, 7685.91, 8.25, 0.00, null, -8.25, + 971.30); + checkInst(model, 33, 33, LocalDate.of(2019, 2, 3), 169, 31, 50.00, 50.00, null, 0.96745672, 48.37, 7644.12, 8.21, 0.00, null, -8.21, + 971.30); + checkInst(model, 34, 34, LocalDate.of(2019, 2, 4), 168, 32, 50.00, 50.00, null, 0.96642476, 48.32, 7602.28, 8.16, 0.00, null, -8.16, + 971.30); + checkInst(model, 35, 35, LocalDate.of(2019, 2, 5), 167, 33, 50.00, 50.00, null, 0.96539390, 48.27, 7560.40, 8.12, 0.00, null, -8.12, + 971.30); + checkInst(model, 36, 36, LocalDate.of(2019, 2, 6), 166, 34, 50.00, 50.00, null, 0.96436413, 48.22, 7518.47, 8.07, 0.00, null, -8.07, + 971.30); + checkInst(model, 37, 37, LocalDate.of(2019, 2, 7), 165, 35, 50.00, 50.00, null, 0.96333547, 48.17, 7476.50, 8.03, 0.00, null, -8.03, + 971.30); + checkInst(model, 38, 38, LocalDate.of(2019, 2, 8), 164, 36, 50.00, 50.00, null, 0.96230790, 48.12, 7434.48, 7.98, 0.00, null, -7.98, + 971.30); + checkInst(model, 39, 39, LocalDate.of(2019, 2, 9), 163, 37, 50.00, 50.00, null, 0.96128143, 48.06, 7392.42, 7.94, 0.00, null, -7.94, + 971.30); + checkInst(model, 40, 40, LocalDate.of(2019, 2, 10), 162, 38, 50.00, 50.00, null, 0.96025606, 48.01, 7350.31, 7.89, 0.00, null, + -7.89, 971.30); + checkInst(model, 41, 41, LocalDate.of(2019, 2, 11), 161, 39, 50.00, 50.00, null, 0.95923178, 47.96, 7308.16, 7.85, 0.00, null, + -7.85, 971.30); + checkInst(model, 42, 42, LocalDate.of(2019, 2, 12), 160, 40, 50.00, 50.00, null, 0.95820859, 47.91, 7265.97, 7.80, 0.00, null, + -7.80, 971.30); + checkInst(model, 43, 43, LocalDate.of(2019, 2, 13), 159, 41, 50.00, 50.00, null, 0.95718649, 47.86, 7223.72, 7.76, 0.00, null, + -7.76, 971.30); + checkInst(model, 44, 44, LocalDate.of(2019, 2, 14), 158, 42, 50.00, 50.00, null, 0.95616548, 47.81, 7181.44, 7.71, 0.00, null, + -7.71, 971.30); + checkInst(model, 45, 45, LocalDate.of(2019, 2, 15), 157, 43, 50.00, 50.00, null, 0.95514557, 47.76, 7139.11, 7.67, 0.00, null, + -7.67, 971.30); + checkInst(model, 46, 46, LocalDate.of(2019, 2, 16), 156, 44, 50.00, 50.00, null, 0.95412674, 47.71, 7096.73, 7.62, 0.00, null, + -7.62, 971.30); + checkInst(model, 47, 47, LocalDate.of(2019, 2, 17), 155, 45, 50.00, 50.00, null, 0.95310899, 47.66, 7054.31, 7.58, 0.00, null, + -7.58, 971.30); + checkInst(model, 48, 48, LocalDate.of(2019, 2, 18), 154, 46, 50.00, 50.00, null, 0.95209233, 47.60, 7011.84, 7.53, 0.00, null, + -7.53, 971.30); + checkInst(model, 49, 49, LocalDate.of(2019, 2, 19), 153, 47, 50.00, 50.00, null, 0.95107676, 47.55, 6969.33, 7.49, 0.00, null, + -7.49, 971.30); + checkInst(model, 50, 50, LocalDate.of(2019, 2, 20), 152, 48, 50.00, 50.00, null, 0.95006227, 47.50, 6926.77, 7.44, 0.00, null, + -7.44, 971.30); + checkInst(model, 51, 51, LocalDate.of(2019, 2, 21), 151, 49, 50.00, 50.00, null, 0.94904886, 47.45, 6884.17, 7.40, 0.00, null, + -7.40, 971.30); + checkInst(model, 52, 52, LocalDate.of(2019, 2, 22), 150, 50, 50.00, 50.00, null, 0.94803653, 47.40, 6841.52, 7.35, 0.00, null, + -7.35, 971.30); + checkInst(model, 53, 53, LocalDate.of(2019, 2, 23), 149, 51, 50.00, 50.00, null, 0.94702529, 47.35, 6798.82, 7.31, 0.00, null, + -7.31, 971.30); + checkInst(model, 54, 54, LocalDate.of(2019, 2, 24), 148, 52, 50.00, 50.00, null, 0.94601512, 47.30, 6756.08, 7.26, 0.00, null, + -7.26, 971.30); + checkInst(model, 55, 55, LocalDate.of(2019, 2, 25), 147, 53, 50.00, 50.00, null, 0.94500603, 47.25, 6713.30, 7.21, 0.00, null, + -7.21, 971.30); + checkInst(model, 56, 56, LocalDate.of(2019, 2, 26), 146, 54, 50.00, 50.00, null, 0.94399801, 47.20, 6670.47, 7.17, 0.00, null, + -7.17, 971.30); + checkInst(model, 57, 57, LocalDate.of(2019, 2, 27), 145, 55, 50.00, 50.00, null, 0.94299107, 47.15, 6627.59, 7.12, 0.00, null, + -7.12, 971.30); + checkInst(model, 58, 58, LocalDate.of(2019, 2, 28), 144, 56, 50.00, 50.00, null, 0.94198521, 47.10, 6584.67, 7.08, 0.00, null, + -7.08, 971.30); + checkInst(model, 59, 59, LocalDate.of(2019, 3, 1), 143, 57, 50.00, 50.00, null, 0.94098042, 47.05, 6541.70, 7.03, 0.00, null, -7.03, + 971.30); + checkInst(model, 60, 60, LocalDate.of(2019, 3, 2), 142, 58, 50.00, 50.00, null, 0.93997669, 47.00, 6498.68, 6.99, 0.00, null, -6.99, + 971.30); + checkInst(model, 61, 61, LocalDate.of(2019, 3, 3), 141, 59, 50.00, 50.00, null, 0.93897404, 46.95, 6455.62, 6.94, 0.00, null, -6.94, + 971.30); + checkInst(model, 62, 62, LocalDate.of(2019, 3, 4), 140, 60, 50.00, 50.00, null, 0.93797246, 46.90, 6412.51, 6.89, 0.00, null, -6.89, + 971.30); + checkInst(model, 63, 63, LocalDate.of(2019, 3, 5), 139, 61, 50.00, 50.00, null, 0.93697195, 46.85, 6369.36, 6.85, 0.00, null, -6.85, + 971.30); + checkInst(model, 64, 64, LocalDate.of(2019, 3, 6), 138, 62, 50.00, 50.00, null, 0.93597251, 46.80, 6326.16, 6.80, 0.00, null, -6.80, + 971.30); + checkInst(model, 65, 65, LocalDate.of(2019, 3, 7), 137, 63, 50.00, 50.00, null, 0.93497413, 46.75, 6282.92, 6.76, 0.00, null, -6.76, + 971.30); + checkInst(model, 66, 66, LocalDate.of(2019, 3, 8), 136, 64, 50.00, 50.00, null, 0.93397681, 46.70, 6239.63, 6.71, 0.00, null, -6.71, + 971.30); + checkInst(model, 67, 67, LocalDate.of(2019, 3, 9), 135, 65, 50.00, 50.00, null, 0.93298056, 46.65, 6196.29, 6.66, 0.00, null, -6.66, + 971.30); + checkInst(model, 68, 68, LocalDate.of(2019, 3, 10), 134, 66, 50.00, 50.00, null, 0.93198538, 46.60, 6152.91, 6.62, 0.00, null, + -6.62, 971.30); + checkInst(model, 69, 69, LocalDate.of(2019, 3, 11), 133, 67, 50.00, 50.00, null, 0.93099125, 46.55, 6109.48, 6.57, 0.00, null, + -6.57, 971.30); + checkInst(model, 70, 70, LocalDate.of(2019, 3, 12), 132, 68, 50.00, 50.00, null, 0.92999818, 46.50, 6066.00, 6.52, 0.00, null, + -6.52, 971.30); + checkInst(model, 71, 71, LocalDate.of(2019, 3, 13), 131, 69, 50.00, 50.00, null, 0.92900618, 46.45, 6022.48, 6.48, 0.00, null, + -6.48, 971.30); + checkInst(model, 72, 72, LocalDate.of(2019, 3, 14), 130, 70, 50.00, 50.00, null, 0.92801523, 46.40, 5978.91, 6.43, 0.00, null, + -6.43, 971.30); + checkInst(model, 73, 73, LocalDate.of(2019, 3, 15), 129, 71, 50.00, 50.00, null, 0.92702534, 46.35, 5935.29, 6.38, 0.00, null, + -6.38, 971.30); + checkInst(model, 74, 74, LocalDate.of(2019, 3, 16), 128, 72, 50.00, 50.00, null, 0.92603650, 46.30, 5891.63, 6.34, 0.00, null, + -6.34, 971.30); + checkInst(model, 75, 75, LocalDate.of(2019, 3, 17), 127, 73, 50.00, 50.00, null, 0.92504872, 46.25, 5847.92, 6.29, 0.00, null, + -6.29, 971.30); + checkInst(model, 76, 76, LocalDate.of(2019, 3, 18), 126, 74, 50.00, 50.00, null, 0.92406200, 46.20, 5804.17, 6.24, 0.00, null, + -6.24, 971.30); + checkInst(model, 77, 77, LocalDate.of(2019, 3, 19), 125, 75, 50.00, 50.00, null, 0.92307632, 46.15, 5760.36, 6.20, 0.00, null, + -6.20, 971.30); + checkInst(model, 78, 78, LocalDate.of(2019, 3, 20), 124, 76, 50.00, 50.00, null, 0.92209170, 46.10, 5716.52, 6.15, 0.00, null, + -6.15, 971.30); + checkInst(model, 79, 79, LocalDate.of(2019, 3, 21), 123, 77, 50.00, 50.00, null, 0.92110813, 46.06, 5672.62, 6.10, 0.00, null, + -6.10, 971.30); + checkInst(model, 80, 80, LocalDate.of(2019, 3, 22), 122, 78, 50.00, 50.00, null, 0.92012560, 46.01, 5628.68, 6.06, 0.00, null, + -6.06, 971.30); + checkInst(model, 81, 81, LocalDate.of(2019, 3, 23), 121, 79, 50.00, 50.00, null, 0.91914413, 45.96, 5584.69, 6.01, 0.00, null, + -6.01, 971.30); + checkInst(model, 82, 82, LocalDate.of(2019, 3, 24), 120, 80, 50.00, 50.00, null, 0.91816370, 45.91, 5540.65, 5.96, 0.00, null, + -5.96, 971.30); + checkInst(model, 83, 83, LocalDate.of(2019, 3, 25), 119, 81, 50.00, 50.00, null, 0.91718432, 45.86, 5496.57, 5.92, 0.00, null, + -5.92, 971.30); + checkInst(model, 84, 84, LocalDate.of(2019, 3, 26), 118, 82, 50.00, 50.00, null, 0.91620598, 45.81, 5452.44, 5.87, 0.00, null, + -5.87, 971.30); + checkInst(model, 85, 85, LocalDate.of(2019, 3, 27), 117, 83, 50.00, 50.00, null, 0.91522868, 45.76, 5408.26, 5.82, 0.00, null, + -5.82, 971.30); + checkInst(model, 86, 86, LocalDate.of(2019, 3, 28), 116, 84, 50.00, 50.00, null, 0.91425243, 45.71, 5364.03, 5.78, 0.00, null, + -5.78, 971.30); + checkInst(model, 87, 87, LocalDate.of(2019, 3, 29), 115, 85, 50.00, 50.00, null, 0.91327722, 45.66, 5319.76, 5.73, 0.00, null, + -5.73, 971.30); + checkInst(model, 88, 88, LocalDate.of(2019, 3, 30), 114, 86, 50.00, 50.00, null, 0.91230305, 45.62, 5275.44, 5.68, 0.00, null, + -5.68, 971.30); + checkInst(model, 89, 89, LocalDate.of(2019, 3, 31), 113, 87, 50.00, 50.00, null, 0.91132992, 45.57, 5231.08, 5.63, 0.00, null, + -5.63, 971.30); + checkInst(model, 90, 90, LocalDate.of(2019, 4, 1), 112, 88, 50.00, 50.00, null, 0.91035783, 45.52, 5186.66, 5.59, 0.00, null, -5.59, + 971.30); + checkInst(model, 91, 91, LocalDate.of(2019, 4, 2), 111, 89, 50.00, 50.00, null, 0.90938677, 45.47, 5142.20, 5.54, 0.00, null, -5.54, + 971.30); + checkInst(model, 92, 92, LocalDate.of(2019, 4, 3), 110, 90, 50.00, 50.00, null, 0.90841675, 45.42, 5097.69, 5.49, 0.00, null, -5.49, + 971.30); + checkInst(model, 93, 93, LocalDate.of(2019, 4, 4), 109, 91, 50.00, 50.00, null, 0.90744776, 45.37, 5053.13, 5.44, 0.00, null, -5.44, + 971.30); + checkInst(model, 94, 94, LocalDate.of(2019, 4, 5), 108, 92, 50.00, 50.00, null, 0.90647981, 45.32, 5008.53, 5.40, 0.00, null, -5.40, + 971.30); + checkInst(model, 95, 95, LocalDate.of(2019, 4, 6), 107, 93, 50.00, 50.00, null, 0.90551289, 45.28, 4963.88, 5.35, 0.00, null, -5.35, + 971.30); + checkInst(model, 96, 96, LocalDate.of(2019, 4, 7), 106, 94, 50.00, 50.00, null, 0.90454700, 45.23, 4919.18, 5.30, 0.00, null, -5.30, + 971.30); + checkInst(model, 97, 97, LocalDate.of(2019, 4, 8), 105, 95, 50.00, 50.00, null, 0.90358215, 45.18, 4874.43, 5.25, 0.00, null, -5.25, + 971.30); + checkInst(model, 98, 98, LocalDate.of(2019, 4, 9), 104, 96, 50.00, 50.00, null, 0.90261832, 45.13, 4829.64, 5.20, 0.00, null, -5.20, + 971.30); + checkInst(model, 99, 99, LocalDate.of(2019, 4, 10), 103, 97, 50.00, 50.00, null, 0.90165552, 45.08, 4784.79, 5.16, 0.00, null, + -5.16, 971.30); + checkInst(model, 100, 100, LocalDate.of(2019, 4, 11), 102, 98, 50.00, 50.00, null, 0.90069374, 45.03, 4739.90, 5.11, 0.00, null, + -5.11, 971.30); + checkInst(model, 101, 101, LocalDate.of(2019, 4, 12), 101, 99, 50.00, 50.00, null, 0.89973299, 44.99, 4694.96, 5.06, 0.00, null, + -5.06, 971.30); + checkInst(model, 102, 102, LocalDate.of(2019, 4, 13), 100, 100, 50.00, 50.00, null, 0.89877327, 44.94, 4649.98, 5.01, 0.00, null, + -5.01, 971.30); + checkInst(model, 103, 103, LocalDate.of(2019, 4, 14), 99, 101, 50.00, 50.00, null, 0.89781457, 44.89, 4604.94, 4.97, 0.00, null, + -4.97, 971.30); + checkInst(model, 104, 104, LocalDate.of(2019, 4, 15), 98, 102, 50.00, 50.00, null, 0.89685689, 44.84, 4559.86, 4.92, 0.00, null, + -4.92, 971.30); + checkInst(model, 105, 105, LocalDate.of(2019, 4, 16), 97, 103, 50.00, 50.00, null, 0.89590024, 44.80, 4514.73, 4.87, 0.00, null, + -4.87, 971.30); + checkInst(model, 106, 106, LocalDate.of(2019, 4, 17), 96, 104, 50.00, 50.00, null, 0.89494460, 44.75, 4469.55, 4.82, 0.00, null, + -4.82, 971.30); + checkInst(model, 107, 107, LocalDate.of(2019, 4, 18), 95, 105, 50.00, 50.00, null, 0.89398999, 44.70, 4424.32, 4.77, 0.00, null, + -4.77, 971.30); + checkInst(model, 108, 108, LocalDate.of(2019, 4, 19), 94, 106, 50.00, 50.00, null, 0.89303639, 44.65, 4379.05, 4.72, 0.00, null, + -4.72, 971.30); + checkInst(model, 109, 109, LocalDate.of(2019, 4, 20), 93, 107, 50.00, 50.00, null, 0.89208381, 44.60, 4333.72, 4.68, 0.00, null, + -4.68, 971.30); + checkInst(model, 110, 110, LocalDate.of(2019, 4, 21), 92, 108, 50.00, 50.00, null, 0.89113225, 44.56, 4288.35, 4.63, 0.00, null, + -4.63, 971.30); + checkInst(model, 111, 111, LocalDate.of(2019, 4, 22), 91, 109, 50.00, 50.00, null, 0.89018170, 44.51, 4242.93, 4.58, 0.00, null, + -4.58, 971.30); + checkInst(model, 112, 112, LocalDate.of(2019, 4, 23), 90, 110, 50.00, 50.00, null, 0.88923216, 44.46, 4197.46, 4.53, 0.00, null, + -4.53, 971.30); + checkInst(model, 113, 113, LocalDate.of(2019, 4, 24), 89, 111, 50.00, 50.00, null, 0.88828364, 44.41, 4151.94, 4.48, 0.00, null, + -4.48, 971.30); + checkInst(model, 114, 114, LocalDate.of(2019, 4, 25), 88, 112, 50.00, 50.00, null, 0.88733613, 44.37, 4106.38, 4.43, 0.00, null, + -4.43, 971.30); + checkInst(model, 115, 115, LocalDate.of(2019, 4, 26), 87, 113, 50.00, 50.00, null, 0.88638963, 44.32, 4060.76, 4.38, 0.00, null, + -4.38, 971.30); + checkInst(model, 116, 116, LocalDate.of(2019, 4, 27), 86, 114, 50.00, 50.00, null, 0.88544414, 44.27, 4015.10, 4.34, 0.00, null, + -4.34, 971.30); + checkInst(model, 117, 117, LocalDate.of(2019, 4, 28), 85, 115, 50.00, 50.00, null, 0.88449966, 44.22, 3969.38, 4.29, 0.00, null, + -4.29, 971.30); + checkInst(model, 118, 118, LocalDate.of(2019, 4, 29), 84, 116, 50.00, 50.00, null, 0.88355619, 44.18, 3923.62, 4.24, 0.00, null, + -4.24, 971.30); + checkInst(model, 119, 119, LocalDate.of(2019, 4, 30), 83, 117, 50.00, 50.00, null, 0.88261372, 44.13, 3877.81, 4.19, 0.00, null, + -4.19, 971.30); + checkInst(model, 120, 120, LocalDate.of(2019, 5, 1), 82, 118, 50.00, 50.00, null, 0.88167226, 44.08, 3831.95, 4.14, 0.00, null, + -4.14, 971.30); + checkInst(model, 121, 121, LocalDate.of(2019, 5, 2), 81, 119, 50.00, 50.00, null, 0.88073180, 44.04, 3786.04, 4.09, 0.00, null, + -4.09, 971.30); + checkInst(model, 122, 122, LocalDate.of(2019, 5, 3), 80, 120, 50.00, 50.00, null, 0.87979234, 43.99, 3740.09, 4.04, 0.00, null, + -4.04, 971.30); + checkInst(model, 123, 123, LocalDate.of(2019, 5, 4), 79, 121, 50.00, 50.00, null, 0.87885389, 43.94, 3694.08, 3.99, 0.00, null, + -3.99, 971.30); + checkInst(model, 124, 124, LocalDate.of(2019, 5, 5), 78, 122, 50.00, 50.00, null, 0.87791644, 43.90, 3648.03, 3.94, 0.00, null, + -3.94, 971.30); + checkInst(model, 125, 125, LocalDate.of(2019, 5, 6), 77, 123, 50.00, 50.00, null, 0.87697999, 43.85, 3601.92, 3.90, 0.00, null, + -3.90, 971.30); + checkInst(model, 126, 126, LocalDate.of(2019, 5, 7), 76, 124, 50.00, 50.00, null, 0.87604453, 43.80, 3555.77, 3.85, 0.00, null, + -3.85, 971.30); + checkInst(model, 127, 127, LocalDate.of(2019, 5, 8), 75, 125, 50.00, 50.00, null, 0.87511008, 43.76, 3509.56, 3.80, 0.00, null, + -3.80, 971.30); + checkInst(model, 128, 128, LocalDate.of(2019, 5, 9), 74, 126, 50.00, 50.00, null, 0.87417662, 43.71, 3463.31, 3.75, 0.00, null, + -3.75, 971.30); + checkInst(model, 129, 129, LocalDate.of(2019, 5, 10), 73, 127, 50.00, 50.00, null, 0.87324416, 43.66, 3417.01, 3.70, 0.00, null, + -3.70, 971.30); + checkInst(model, 130, 130, LocalDate.of(2019, 5, 11), 72, 128, 50.00, 50.00, null, 0.87231269, 43.62, 3370.66, 3.65, 0.00, null, + -3.65, 971.30); + checkInst(model, 131, 131, LocalDate.of(2019, 5, 12), 71, 129, 50.00, 50.00, null, 0.87138221, 43.57, 3324.26, 3.60, 0.00, null, + -3.60, 971.30); + checkInst(model, 132, 132, LocalDate.of(2019, 5, 13), 70, 130, 50.00, 50.00, null, 0.87045273, 43.52, 3277.81, 3.55, 0.00, null, + -3.55, 971.30); + checkInst(model, 133, 133, LocalDate.of(2019, 5, 14), 69, 131, 50.00, 50.00, null, 0.86952424, 43.48, 3231.31, 3.50, 0.00, null, + -3.50, 971.30); + checkInst(model, 134, 134, LocalDate.of(2019, 5, 15), 68, 132, 50.00, 50.00, null, 0.86859674, 43.43, 3184.76, 3.45, 0.00, null, + -3.45, 971.30); + checkInst(model, 135, 135, LocalDate.of(2019, 5, 16), 67, 133, 50.00, 50.00, null, 0.86767023, 43.38, 3138.16, 3.40, 0.00, null, + -3.40, 971.30); + checkInst(model, 136, 136, LocalDate.of(2019, 5, 17), 66, 134, 50.00, 50.00, null, 0.86674471, 43.34, 3091.51, 3.35, 0.00, null, + -3.35, 971.30); + checkInst(model, 137, 137, LocalDate.of(2019, 5, 18), 65, 135, 50.00, 50.00, null, 0.86582017, 43.29, 3044.81, 3.30, 0.00, null, + -3.30, 971.30); + checkInst(model, 138, 138, LocalDate.of(2019, 5, 19), 64, 136, 50.00, 50.00, null, 0.86489662, 43.24, 2998.06, 3.25, 0.00, null, + -3.25, 971.30); + checkInst(model, 139, 139, LocalDate.of(2019, 5, 20), 63, 137, 50.00, 50.00, null, 0.86397406, 43.20, 2951.26, 3.20, 0.00, null, + -3.20, 971.30); + checkInst(model, 140, 140, LocalDate.of(2019, 5, 21), 62, 138, 50.00, 50.00, null, 0.86305248, 43.15, 2904.42, 3.15, 0.00, null, + -3.15, 971.30); + checkInst(model, 141, 141, LocalDate.of(2019, 5, 22), 61, 139, 50.00, 50.00, null, 0.86213188, 43.11, 2857.52, 3.10, 0.00, null, + -3.10, 971.30); + checkInst(model, 142, 142, LocalDate.of(2019, 5, 23), 60, 140, 50.00, 50.00, null, 0.86121227, 43.06, 2810.57, 3.05, 0.00, null, + -3.05, 971.30); + checkInst(model, 143, 143, LocalDate.of(2019, 5, 24), 59, 141, 50.00, 50.00, null, 0.86029363, 43.01, 2763.57, 3.00, 0.00, null, + -3.00, 971.30); + checkInst(model, 144, 144, LocalDate.of(2019, 5, 25), 58, 142, 50.00, 50.00, null, 0.85937598, 42.97, 2716.52, 2.95, 0.00, null, + -2.95, 971.30); + checkInst(model, 145, 145, LocalDate.of(2019, 5, 26), 57, 143, 50.00, 50.00, null, 0.85845930, 42.92, 2669.42, 2.90, 0.00, null, + -2.90, 971.30); + checkInst(model, 146, 146, LocalDate.of(2019, 5, 27), 56, 144, 50.00, 50.00, null, 0.85754361, 42.88, 2622.27, 2.85, 0.00, null, + -2.85, 971.30); + checkInst(model, 147, 147, LocalDate.of(2019, 5, 28), 55, 145, 50.00, 50.00, null, 0.85662889, 42.83, 2575.07, 2.80, 0.00, null, + -2.80, 971.30); + checkInst(model, 148, 148, LocalDate.of(2019, 5, 29), 54, 146, 50.00, 50.00, null, 0.85571514, 42.79, 2527.82, 2.75, 0.00, null, + -2.75, 971.30); + checkInst(model, 149, 149, LocalDate.of(2019, 5, 30), 53, 147, 50.00, 50.00, null, 0.85480237, 42.74, 2480.52, 2.70, 0.00, null, + -2.70, 971.30); + checkInst(model, 150, 150, LocalDate.of(2019, 5, 31), 52, 148, 50.00, 50.00, null, 0.85389057, 42.69, 2433.17, 2.65, 0.00, null, + -2.65, 971.30); + checkInst(model, 151, 151, LocalDate.of(2019, 6, 1), 51, 149, 50.00, 50.00, null, 0.85297975, 42.65, 2385.77, 2.60, 0.00, null, + -2.60, 971.30); + checkInst(model, 152, 152, LocalDate.of(2019, 6, 2), 50, 150, 50.00, 50.00, null, 0.85206990, 42.60, 2338.31, 2.55, 0.00, null, + -2.55, 971.30); + checkInst(model, 153, 153, LocalDate.of(2019, 6, 3), 49, 151, 50.00, 50.00, null, 0.85116101, 42.56, 2290.81, 2.50, 0.00, null, + -2.50, 971.30); + checkInst(model, 154, 154, LocalDate.of(2019, 6, 4), 48, 152, 50.00, 50.00, null, 0.85025310, 42.51, 2243.26, 2.45, 0.00, null, + -2.45, 971.30); + checkInst(model, 155, 155, LocalDate.of(2019, 6, 5), 47, 153, 50.00, 50.00, null, 0.84934616, 42.47, 2195.65, 2.40, 0.00, null, + -2.40, 971.30); + checkInst(model, 156, 156, LocalDate.of(2019, 6, 6), 46, 154, 50.00, 50.00, null, 0.84844018, 42.42, 2148.00, 2.34, 0.00, null, + -2.34, 971.30); + checkInst(model, 157, 157, LocalDate.of(2019, 6, 7), 45, 155, 50.00, 50.00, null, 0.84753517, 42.38, 2100.29, 2.29, 0.00, null, + -2.29, 971.30); + checkInst(model, 158, 158, LocalDate.of(2019, 6, 8), 44, 156, 50.00, 50.00, null, 0.84663113, 42.33, 2052.53, 2.24, 0.00, null, + -2.24, 971.30); + checkInst(model, 159, 159, LocalDate.of(2019, 6, 9), 43, 157, 50.00, 50.00, null, 0.84572805, 42.29, 2004.73, 2.19, 0.00, null, + -2.19, 971.30); + checkInst(model, 160, 160, LocalDate.of(2019, 6, 10), 42, 158, 50.00, 50.00, null, 0.84482593, 42.24, 1956.87, 2.14, 0.00, null, + -2.14, 971.30); + checkInst(model, 161, 161, LocalDate.of(2019, 6, 11), 41, 159, 50.00, 50.00, null, 0.84392477, 42.20, 1908.96, 2.09, 0.00, null, + -2.09, 971.30); + checkInst(model, 162, 162, LocalDate.of(2019, 6, 12), 40, 160, 50.00, 50.00, null, 0.84302458, 42.15, 1860.99, 2.04, 0.00, null, + -2.04, 971.30); + checkInst(model, 163, 163, LocalDate.of(2019, 6, 13), 39, 161, 50.00, 50.00, null, 0.84212535, 42.11, 1812.98, 1.99, 0.00, null, + -1.99, 971.30); + checkInst(model, 164, 164, LocalDate.of(2019, 6, 14), 38, 162, 50.00, 50.00, null, 0.84122707, 42.06, 1764.92, 1.94, 0.00, null, + -1.94, 971.30); + checkInst(model, 165, 165, LocalDate.of(2019, 6, 15), 37, 163, 50.00, 50.00, null, 0.84032975, 42.02, 1716.80, 1.88, 0.00, null, + -1.88, 971.30); + checkInst(model, 166, 166, LocalDate.of(2019, 6, 16), 36, 164, 50.00, 50.00, null, 0.83943340, 41.97, 1668.64, 1.83, 0.00, null, + -1.83, 971.30); + checkInst(model, 167, 167, LocalDate.of(2019, 6, 17), 35, 165, 50.00, 50.00, null, 0.83853799, 41.93, 1620.42, 1.78, 0.00, null, + -1.78, 971.30); + checkInst(model, 168, 168, LocalDate.of(2019, 6, 18), 34, 166, 50.00, 50.00, null, 0.83764354, 41.88, 1572.15, 1.73, 0.00, null, + -1.73, 971.30); + checkInst(model, 169, 169, LocalDate.of(2019, 6, 19), 33, 167, 50.00, 50.00, null, 0.83675005, 41.84, 1523.83, 1.68, 0.00, null, + -1.68, 971.30); + checkInst(model, 170, 170, LocalDate.of(2019, 6, 20), 32, 168, 50.00, 50.00, null, 0.83585751, 41.79, 1475.45, 1.63, 0.00, null, + -1.63, 971.30); + checkInst(model, 171, 171, LocalDate.of(2019, 6, 21), 31, 169, 50.00, 50.00, null, 0.83496592, 41.75, 1427.03, 1.58, 0.00, null, + -1.58, 971.30); + checkInst(model, 172, 172, LocalDate.of(2019, 6, 22), 30, 170, 50.00, 50.00, null, 0.83407528, 41.70, 1378.55, 1.52, 0.00, null, + -1.52, 971.30); + checkInst(model, 173, 173, LocalDate.of(2019, 6, 23), 29, 171, 50.00, 50.00, null, 0.83318560, 41.66, 1330.02, 1.47, 0.00, null, + -1.47, 971.30); + checkInst(model, 174, 174, LocalDate.of(2019, 6, 24), 28, 172, 50.00, 50.00, null, 0.83229686, 41.61, 1281.45, 1.42, 0.00, null, + -1.42, 971.30); + checkInst(model, 175, 175, LocalDate.of(2019, 6, 25), 27, 173, 50.00, 50.00, null, 0.83140907, 41.57, 1232.81, 1.37, 0.00, null, + -1.37, 971.30); + checkInst(model, 176, 176, LocalDate.of(2019, 6, 26), 26, 174, 50.00, 50.00, null, 0.83052222, 41.53, 1184.13, 1.32, 0.00, null, + -1.32, 971.30); + checkInst(model, 177, 177, LocalDate.of(2019, 6, 27), 25, 175, 50.00, 50.00, null, 0.82963633, 41.48, 1135.39, 1.26, 0.00, null, + -1.26, 971.30); + checkInst(model, 178, 178, LocalDate.of(2019, 6, 28), 24, 176, 50.00, 50.00, null, 0.82875137, 41.44, 1086.61, 1.21, 0.00, null, + -1.21, 971.30); + checkInst(model, 179, 179, LocalDate.of(2019, 6, 29), 23, 177, 50.00, 50.00, null, 0.82786736, 41.39, 1037.77, 1.16, 0.00, null, + -1.16, 971.30); + checkInst(model, 180, 180, LocalDate.of(2019, 6, 30), 22, 178, 50.00, 50.00, null, 0.82698430, 41.35, 988.88, 1.11, 0.00, null, + -1.11, 971.30); + checkInst(model, 181, 181, LocalDate.of(2019, 7, 1), 21, 179, 50.00, 50.00, null, 0.82610217, 41.31, 939.93, 1.06, 0.00, null, + -1.06, 971.30); + checkInst(model, 182, 182, LocalDate.of(2019, 7, 2), 20, 180, 50.00, 50.00, null, 0.82522099, 41.26, 890.93, 1.00, 0.00, null, + -1.00, 971.30); + checkInst(model, 183, 183, LocalDate.of(2019, 7, 3), 19, 181, 50.00, 50.00, null, 0.82434075, 41.22, 841.89, 0.95, 0.00, null, + -0.95, 971.30); + checkInst(model, 184, 184, LocalDate.of(2019, 7, 4), 18, 182, 50.00, 50.00, null, 0.82346144, 41.17, 792.79, 0.90, 0.00, null, + -0.90, 971.30); + checkInst(model, 185, 185, LocalDate.of(2019, 7, 5), 17, 183, 50.00, 50.00, null, 0.82258308, 41.13, 743.63, 0.85, 0.00, null, + -0.85, 971.30); + checkInst(model, 186, 186, LocalDate.of(2019, 7, 6), 16, 184, 50.00, 50.00, null, 0.82170565, 41.09, 694.43, 0.79, 0.00, null, + -0.79, 971.30); + checkInst(model, 187, 187, LocalDate.of(2019, 7, 7), 15, 185, 50.00, 50.00, null, 0.82082916, 41.04, 645.17, 0.74, 0.00, null, + -0.74, 971.30); + checkInst(model, 188, 188, LocalDate.of(2019, 7, 8), 14, 186, 50.00, 50.00, null, 0.81995360, 41.00, 595.86, 0.69, 0.00, null, + -0.69, 971.30); + checkInst(model, 189, 189, LocalDate.of(2019, 7, 9), 13, 187, 50.00, 50.00, null, 0.81907897, 40.95, 546.49, 0.64, 0.00, null, + -0.64, 971.30); + checkInst(model, 190, 190, LocalDate.of(2019, 7, 10), 12, 188, 50.00, 50.00, null, 0.81820528, 40.91, 497.08, 0.58, 0.00, null, + -0.58, 971.30); + checkInst(model, 191, 191, LocalDate.of(2019, 7, 11), 11, 189, 50.00, 50.00, null, 0.81733252, 40.87, 447.61, 0.53, 0.00, null, + -0.53, 971.30); + checkInst(model, 192, 192, LocalDate.of(2019, 7, 12), 10, 190, 50.00, 50.00, null, 0.81646069, 40.82, 398.08, 0.48, 0.00, null, + -0.48, 971.30); + checkInst(model, 193, 193, LocalDate.of(2019, 7, 13), 9, 191, 50.00, 50.00, null, 0.81558979, 40.78, 348.51, 0.43, 0.00, null, + -0.43, 971.30); + checkInst(model, 194, 194, LocalDate.of(2019, 7, 14), 8, 192, 50.00, 50.00, null, 0.81471983, 40.74, 298.88, 0.37, 0.00, null, + -0.37, 971.30); + checkInst(model, 195, 195, LocalDate.of(2019, 7, 15), 7, 193, 50.00, 50.00, null, 0.81385078, 40.69, 249.20, 0.32, 0.00, null, + -0.32, 971.30); + checkInst(model, 196, 196, LocalDate.of(2019, 7, 16), 6, 194, 50.00, 50.00, null, 0.81298267, 40.65, 199.47, 0.27, 0.00, null, + -0.27, 971.30); + checkInst(model, 197, 197, LocalDate.of(2019, 7, 17), 5, 195, 50.00, 50.00, null, 0.81211548, 40.61, 149.68, 0.21, 0.00, null, + -0.21, 971.30); + checkInst(model, 198, 198, LocalDate.of(2019, 7, 18), 4, 196, 50.00, 50.00, null, 0.81124922, 40.56, 99.84, 0.16, 0.00, null, -0.16, + 971.30); + checkInst(model, 199, 199, LocalDate.of(2019, 7, 19), 3, 197, 50.00, 50.00, null, 0.81038388, 40.52, 49.95, 0.11, 0.00, null, -0.11, + 971.30); + + assertEquals(200, model.payments().size(), "disbursement + 199 regular (period 200 removed, forecast was 0)"); + } + + @Test + void testLessPayment_term200_originationFee1000_netDisbursement9000_pay40() { + final ProjectedAmortizationScheduleModel model = generateModel(); + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(1), new BigDecimal("40")); + + checkInst(model, 0, 0, EXPECTED_DISBURSEMENT_DATE, 201, 0, -9000.00, null, null, 1.00000000, -9000.00, 9000.00, null, null, null, + null, 1000.00); + + checkInst(model, 1, 1, LocalDate.of(2019, 1, 2), 200, 0, 50.00, 50.00, 40.00, 1.00000000, 40.00, 8959.61, 9.61, 7.69, 7.69, -1.92, + 992.31); + + checkInst(model, 2, 2, LocalDate.of(2019, 1, 3), 199, 1, 50.00, 50.00, null, 0.99893332, 49.95, 8919.18, 9.57, 0.00, null, -9.57, + 992.31); + checkInst(model, 3, 3, LocalDate.of(2019, 1, 4), 198, 2, 50.00, 50.00, null, 0.99786779, 49.89, 8878.70, 9.52, 0.00, null, -9.52, + 992.31); + checkInst(model, 4, 4, LocalDate.of(2019, 1, 5), 197, 3, 50.00, 50.00, null, 0.99680339, 49.84, 8838.18, 9.48, 0.00, null, -9.48, + 992.31); + checkInst(model, 5, 5, LocalDate.of(2019, 1, 6), 196, 4, 50.00, 50.00, null, 0.99574012, 49.79, 8797.62, 9.44, 0.00, null, -9.44, + 992.31); + checkInst(model, 6, 6, LocalDate.of(2019, 1, 7), 195, 5, 50.00, 50.00, null, 0.99467799, 49.73, 8757.01, 9.39, 0.00, null, -9.39, + 992.31); + checkInst(model, 7, 7, LocalDate.of(2019, 1, 8), 194, 6, 50.00, 50.00, null, 0.99361699, 49.68, 8716.36, 9.35, 0.00, null, -9.35, + 992.31); + checkInst(model, 8, 8, LocalDate.of(2019, 1, 9), 193, 7, 50.00, 50.00, null, 0.99255712, 49.63, 8675.67, 9.31, 0.00, null, -9.31, + 992.31); + checkInst(model, 9, 9, LocalDate.of(2019, 1, 10), 192, 8, 50.00, 50.00, null, 0.99149839, 49.57, 8634.94, 9.26, 0.00, null, -9.26, + 992.31); + checkInst(model, 10, 10, LocalDate.of(2019, 1, 11), 191, 9, 50.00, 50.00, null, 0.99044078, 49.52, 8594.16, 9.22, 0.00, null, -9.22, + 992.31); + checkInst(model, 11, 11, LocalDate.of(2019, 1, 12), 190, 10, 50.00, 50.00, null, 0.98938430, 49.47, 8553.33, 9.18, 0.00, null, + -9.18, 992.31); + checkInst(model, 12, 12, LocalDate.of(2019, 1, 13), 189, 11, 50.00, 50.00, null, 0.98832895, 49.42, 8512.47, 9.13, 0.00, null, + -9.13, 992.31); + checkInst(model, 13, 13, LocalDate.of(2019, 1, 14), 188, 12, 50.00, 50.00, null, 0.98727472, 49.36, 8471.56, 9.09, 0.00, null, + -9.09, 992.31); + checkInst(model, 14, 14, LocalDate.of(2019, 1, 15), 187, 13, 50.00, 50.00, null, 0.98622162, 49.31, 8430.60, 9.05, 0.00, null, + -9.05, 992.31); + checkInst(model, 15, 15, LocalDate.of(2019, 1, 16), 186, 14, 50.00, 50.00, null, 0.98516964, 49.26, 8389.61, 9.00, 0.00, null, + -9.00, 992.31); + checkInst(model, 16, 16, LocalDate.of(2019, 1, 17), 185, 15, 50.00, 50.00, null, 0.98411879, 49.21, 8348.56, 8.96, 0.00, null, + -8.96, 992.31); + checkInst(model, 17, 17, LocalDate.of(2019, 1, 18), 184, 16, 50.00, 50.00, null, 0.98306905, 49.15, 8307.48, 8.91, 0.00, null, + -8.91, 992.31); + checkInst(model, 18, 18, LocalDate.of(2019, 1, 19), 183, 17, 50.00, 50.00, null, 0.98202044, 49.10, 8266.35, 8.87, 0.00, null, + -8.87, 992.31); + checkInst(model, 19, 19, LocalDate.of(2019, 1, 20), 182, 18, 50.00, 50.00, null, 0.98097294, 49.05, 8225.18, 8.83, 0.00, null, + -8.83, 992.31); + checkInst(model, 20, 20, LocalDate.of(2019, 1, 21), 181, 19, 50.00, 50.00, null, 0.97992656, 49.00, 8183.96, 8.78, 0.00, null, + -8.78, 992.31); + checkInst(model, 21, 21, LocalDate.of(2019, 1, 22), 180, 20, 50.00, 50.00, null, 0.97888129, 48.94, 8142.70, 8.74, 0.00, null, + -8.74, 992.31); + checkInst(model, 22, 22, LocalDate.of(2019, 1, 23), 179, 21, 50.00, 50.00, null, 0.97783715, 48.89, 8101.39, 8.69, 0.00, null, + -8.69, 992.31); + checkInst(model, 23, 23, LocalDate.of(2019, 1, 24), 178, 22, 50.00, 50.00, null, 0.97679411, 48.84, 8060.04, 8.65, 0.00, null, + -8.65, 992.31); + checkInst(model, 24, 24, LocalDate.of(2019, 1, 25), 177, 23, 50.00, 50.00, null, 0.97575219, 48.79, 8018.65, 8.61, 0.00, null, + -8.61, 992.31); + checkInst(model, 25, 25, LocalDate.of(2019, 1, 26), 176, 24, 50.00, 50.00, null, 0.97471138, 48.74, 7977.21, 8.56, 0.00, null, + -8.56, 992.31); + checkInst(model, 26, 26, LocalDate.of(2019, 1, 27), 175, 25, 50.00, 50.00, null, 0.97367168, 48.68, 7935.73, 8.52, 0.00, null, + -8.52, 992.31); + checkInst(model, 27, 27, LocalDate.of(2019, 1, 28), 174, 26, 50.00, 50.00, null, 0.97263309, 48.63, 7894.21, 8.47, 0.00, null, + -8.47, 992.31); + checkInst(model, 28, 28, LocalDate.of(2019, 1, 29), 173, 27, 50.00, 50.00, null, 0.97159560, 48.58, 7852.63, 8.43, 0.00, null, + -8.43, 992.31); + checkInst(model, 29, 29, LocalDate.of(2019, 1, 30), 172, 28, 50.00, 50.00, null, 0.97055922, 48.53, 7811.02, 8.39, 0.00, null, + -8.39, 992.31); + checkInst(model, 30, 30, LocalDate.of(2019, 1, 31), 171, 29, 50.00, 50.00, null, 0.96952395, 48.48, 7769.36, 8.34, 0.00, null, + -8.34, 992.31); + checkInst(model, 31, 31, LocalDate.of(2019, 2, 1), 170, 30, 50.00, 50.00, null, 0.96848979, 48.42, 7727.66, 8.30, 0.00, null, -8.30, + 992.31); + checkInst(model, 32, 32, LocalDate.of(2019, 2, 2), 169, 31, 50.00, 50.00, null, 0.96745672, 48.37, 7685.91, 8.25, 0.00, null, -8.25, + 992.31); + checkInst(model, 33, 33, LocalDate.of(2019, 2, 3), 168, 32, 50.00, 50.00, null, 0.96642476, 48.32, 7644.12, 8.21, 0.00, null, -8.21, + 992.31); + checkInst(model, 34, 34, LocalDate.of(2019, 2, 4), 167, 33, 50.00, 50.00, null, 0.96539390, 48.27, 7602.28, 8.16, 0.00, null, -8.16, + 992.31); + checkInst(model, 35, 35, LocalDate.of(2019, 2, 5), 166, 34, 50.00, 50.00, null, 0.96436413, 48.22, 7560.40, 8.12, 0.00, null, -8.12, + 992.31); + checkInst(model, 36, 36, LocalDate.of(2019, 2, 6), 165, 35, 50.00, 50.00, null, 0.96333547, 48.17, 7518.47, 8.07, 0.00, null, -8.07, + 992.31); + checkInst(model, 37, 37, LocalDate.of(2019, 2, 7), 164, 36, 50.00, 50.00, null, 0.96230790, 48.12, 7476.50, 8.03, 0.00, null, -8.03, + 992.31); + checkInst(model, 38, 38, LocalDate.of(2019, 2, 8), 163, 37, 50.00, 50.00, null, 0.96128143, 48.06, 7434.48, 7.98, 0.00, null, -7.98, + 992.31); + checkInst(model, 39, 39, LocalDate.of(2019, 2, 9), 162, 38, 50.00, 50.00, null, 0.96025606, 48.01, 7392.42, 7.94, 0.00, null, -7.94, + 992.31); + checkInst(model, 40, 40, LocalDate.of(2019, 2, 10), 161, 39, 50.00, 50.00, null, 0.95923178, 47.96, 7350.31, 7.89, 0.00, null, + -7.89, 992.31); + checkInst(model, 41, 41, LocalDate.of(2019, 2, 11), 160, 40, 50.00, 50.00, null, 0.95820859, 47.91, 7308.16, 7.85, 0.00, null, + -7.85, 992.31); + checkInst(model, 42, 42, LocalDate.of(2019, 2, 12), 159, 41, 50.00, 50.00, null, 0.95718649, 47.86, 7265.97, 7.80, 0.00, null, + -7.80, 992.31); + checkInst(model, 43, 43, LocalDate.of(2019, 2, 13), 158, 42, 50.00, 50.00, null, 0.95616548, 47.81, 7223.72, 7.76, 0.00, null, + -7.76, 992.31); + checkInst(model, 44, 44, LocalDate.of(2019, 2, 14), 157, 43, 50.00, 50.00, null, 0.95514557, 47.76, 7181.44, 7.71, 0.00, null, + -7.71, 992.31); + checkInst(model, 45, 45, LocalDate.of(2019, 2, 15), 156, 44, 50.00, 50.00, null, 0.95412674, 47.71, 7139.11, 7.67, 0.00, null, + -7.67, 992.31); + checkInst(model, 46, 46, LocalDate.of(2019, 2, 16), 155, 45, 50.00, 50.00, null, 0.95310899, 47.66, 7096.73, 7.62, 0.00, null, + -7.62, 992.31); + checkInst(model, 47, 47, LocalDate.of(2019, 2, 17), 154, 46, 50.00, 50.00, null, 0.95209233, 47.60, 7054.31, 7.58, 0.00, null, + -7.58, 992.31); + checkInst(model, 48, 48, LocalDate.of(2019, 2, 18), 153, 47, 50.00, 50.00, null, 0.95107676, 47.55, 7011.84, 7.53, 0.00, null, + -7.53, 992.31); + checkInst(model, 49, 49, LocalDate.of(2019, 2, 19), 152, 48, 50.00, 50.00, null, 0.95006227, 47.50, 6969.33, 7.49, 0.00, null, + -7.49, 992.31); + checkInst(model, 50, 50, LocalDate.of(2019, 2, 20), 151, 49, 50.00, 50.00, null, 0.94904886, 47.45, 6926.77, 7.44, 0.00, null, + -7.44, 992.31); + checkInst(model, 51, 51, LocalDate.of(2019, 2, 21), 150, 50, 50.00, 50.00, null, 0.94803653, 47.40, 6884.17, 7.40, 0.00, null, + -7.40, 992.31); + checkInst(model, 52, 52, LocalDate.of(2019, 2, 22), 149, 51, 50.00, 50.00, null, 0.94702529, 47.35, 6841.52, 7.35, 0.00, null, + -7.35, 992.31); + checkInst(model, 53, 53, LocalDate.of(2019, 2, 23), 148, 52, 50.00, 50.00, null, 0.94601512, 47.30, 6798.82, 7.31, 0.00, null, + -7.31, 992.31); + checkInst(model, 54, 54, LocalDate.of(2019, 2, 24), 147, 53, 50.00, 50.00, null, 0.94500603, 47.25, 6756.08, 7.26, 0.00, null, + -7.26, 992.31); + checkInst(model, 55, 55, LocalDate.of(2019, 2, 25), 146, 54, 50.00, 50.00, null, 0.94399801, 47.20, 6713.30, 7.21, 0.00, null, + -7.21, 992.31); + checkInst(model, 56, 56, LocalDate.of(2019, 2, 26), 145, 55, 50.00, 50.00, null, 0.94299107, 47.15, 6670.47, 7.17, 0.00, null, + -7.17, 992.31); + checkInst(model, 57, 57, LocalDate.of(2019, 2, 27), 144, 56, 50.00, 50.00, null, 0.94198521, 47.10, 6627.59, 7.12, 0.00, null, + -7.12, 992.31); + checkInst(model, 58, 58, LocalDate.of(2019, 2, 28), 143, 57, 50.00, 50.00, null, 0.94098042, 47.05, 6584.67, 7.08, 0.00, null, + -7.08, 992.31); + checkInst(model, 59, 59, LocalDate.of(2019, 3, 1), 142, 58, 50.00, 50.00, null, 0.93997669, 47.00, 6541.70, 7.03, 0.00, null, -7.03, + 992.31); + checkInst(model, 60, 60, LocalDate.of(2019, 3, 2), 141, 59, 50.00, 50.00, null, 0.93897404, 46.95, 6498.68, 6.99, 0.00, null, -6.99, + 992.31); + checkInst(model, 61, 61, LocalDate.of(2019, 3, 3), 140, 60, 50.00, 50.00, null, 0.93797246, 46.90, 6455.62, 6.94, 0.00, null, -6.94, + 992.31); + checkInst(model, 62, 62, LocalDate.of(2019, 3, 4), 139, 61, 50.00, 50.00, null, 0.93697195, 46.85, 6412.51, 6.89, 0.00, null, -6.89, + 992.31); + checkInst(model, 63, 63, LocalDate.of(2019, 3, 5), 138, 62, 50.00, 50.00, null, 0.93597251, 46.80, 6369.36, 6.85, 0.00, null, -6.85, + 992.31); + checkInst(model, 64, 64, LocalDate.of(2019, 3, 6), 137, 63, 50.00, 50.00, null, 0.93497413, 46.75, 6326.16, 6.80, 0.00, null, -6.80, + 992.31); + checkInst(model, 65, 65, LocalDate.of(2019, 3, 7), 136, 64, 50.00, 50.00, null, 0.93397681, 46.70, 6282.92, 6.76, 0.00, null, -6.76, + 992.31); + checkInst(model, 66, 66, LocalDate.of(2019, 3, 8), 135, 65, 50.00, 50.00, null, 0.93298056, 46.65, 6239.63, 6.71, 0.00, null, -6.71, + 992.31); + checkInst(model, 67, 67, LocalDate.of(2019, 3, 9), 134, 66, 50.00, 50.00, null, 0.93198538, 46.60, 6196.29, 6.66, 0.00, null, -6.66, + 992.31); + checkInst(model, 68, 68, LocalDate.of(2019, 3, 10), 133, 67, 50.00, 50.00, null, 0.93099125, 46.55, 6152.91, 6.62, 0.00, null, + -6.62, 992.31); + checkInst(model, 69, 69, LocalDate.of(2019, 3, 11), 132, 68, 50.00, 50.00, null, 0.92999818, 46.50, 6109.48, 6.57, 0.00, null, + -6.57, 992.31); + checkInst(model, 70, 70, LocalDate.of(2019, 3, 12), 131, 69, 50.00, 50.00, null, 0.92900618, 46.45, 6066.00, 6.52, 0.00, null, + -6.52, 992.31); + checkInst(model, 71, 71, LocalDate.of(2019, 3, 13), 130, 70, 50.00, 50.00, null, 0.92801523, 46.40, 6022.48, 6.48, 0.00, null, + -6.48, 992.31); + checkInst(model, 72, 72, LocalDate.of(2019, 3, 14), 129, 71, 50.00, 50.00, null, 0.92702534, 46.35, 5978.91, 6.43, 0.00, null, + -6.43, 992.31); + checkInst(model, 73, 73, LocalDate.of(2019, 3, 15), 128, 72, 50.00, 50.00, null, 0.92603650, 46.30, 5935.29, 6.38, 0.00, null, + -6.38, 992.31); + checkInst(model, 74, 74, LocalDate.of(2019, 3, 16), 127, 73, 50.00, 50.00, null, 0.92504872, 46.25, 5891.63, 6.34, 0.00, null, + -6.34, 992.31); + checkInst(model, 75, 75, LocalDate.of(2019, 3, 17), 126, 74, 50.00, 50.00, null, 0.92406200, 46.20, 5847.92, 6.29, 0.00, null, + -6.29, 992.31); + checkInst(model, 76, 76, LocalDate.of(2019, 3, 18), 125, 75, 50.00, 50.00, null, 0.92307632, 46.15, 5804.17, 6.24, 0.00, null, + -6.24, 992.31); + checkInst(model, 77, 77, LocalDate.of(2019, 3, 19), 124, 76, 50.00, 50.00, null, 0.92209170, 46.10, 5760.36, 6.20, 0.00, null, + -6.20, 992.31); + checkInst(model, 78, 78, LocalDate.of(2019, 3, 20), 123, 77, 50.00, 50.00, null, 0.92110813, 46.06, 5716.52, 6.15, 0.00, null, + -6.15, 992.31); + checkInst(model, 79, 79, LocalDate.of(2019, 3, 21), 122, 78, 50.00, 50.00, null, 0.92012560, 46.01, 5672.62, 6.10, 0.00, null, + -6.10, 992.31); + checkInst(model, 80, 80, LocalDate.of(2019, 3, 22), 121, 79, 50.00, 50.00, null, 0.91914413, 45.96, 5628.68, 6.06, 0.00, null, + -6.06, 992.31); + checkInst(model, 81, 81, LocalDate.of(2019, 3, 23), 120, 80, 50.00, 50.00, null, 0.91816370, 45.91, 5584.69, 6.01, 0.00, null, + -6.01, 992.31); + checkInst(model, 82, 82, LocalDate.of(2019, 3, 24), 119, 81, 50.00, 50.00, null, 0.91718432, 45.86, 5540.65, 5.96, 0.00, null, + -5.96, 992.31); + checkInst(model, 83, 83, LocalDate.of(2019, 3, 25), 118, 82, 50.00, 50.00, null, 0.91620598, 45.81, 5496.57, 5.92, 0.00, null, + -5.92, 992.31); + checkInst(model, 84, 84, LocalDate.of(2019, 3, 26), 117, 83, 50.00, 50.00, null, 0.91522868, 45.76, 5452.44, 5.87, 0.00, null, + -5.87, 992.31); + checkInst(model, 85, 85, LocalDate.of(2019, 3, 27), 116, 84, 50.00, 50.00, null, 0.91425243, 45.71, 5408.26, 5.82, 0.00, null, + -5.82, 992.31); + checkInst(model, 86, 86, LocalDate.of(2019, 3, 28), 115, 85, 50.00, 50.00, null, 0.91327722, 45.66, 5364.03, 5.78, 0.00, null, + -5.78, 992.31); + checkInst(model, 87, 87, LocalDate.of(2019, 3, 29), 114, 86, 50.00, 50.00, null, 0.91230305, 45.62, 5319.76, 5.73, 0.00, null, + -5.73, 992.31); + checkInst(model, 88, 88, LocalDate.of(2019, 3, 30), 113, 87, 50.00, 50.00, null, 0.91132992, 45.57, 5275.44, 5.68, 0.00, null, + -5.68, 992.31); + checkInst(model, 89, 89, LocalDate.of(2019, 3, 31), 112, 88, 50.00, 50.00, null, 0.91035783, 45.52, 5231.08, 5.63, 0.00, null, + -5.63, 992.31); + checkInst(model, 90, 90, LocalDate.of(2019, 4, 1), 111, 89, 50.00, 50.00, null, 0.90938677, 45.47, 5186.66, 5.59, 0.00, null, -5.59, + 992.31); + checkInst(model, 91, 91, LocalDate.of(2019, 4, 2), 110, 90, 50.00, 50.00, null, 0.90841675, 45.42, 5142.20, 5.54, 0.00, null, -5.54, + 992.31); + checkInst(model, 92, 92, LocalDate.of(2019, 4, 3), 109, 91, 50.00, 50.00, null, 0.90744776, 45.37, 5097.69, 5.49, 0.00, null, -5.49, + 992.31); + checkInst(model, 93, 93, LocalDate.of(2019, 4, 4), 108, 92, 50.00, 50.00, null, 0.90647981, 45.32, 5053.13, 5.44, 0.00, null, -5.44, + 992.31); + checkInst(model, 94, 94, LocalDate.of(2019, 4, 5), 107, 93, 50.00, 50.00, null, 0.90551289, 45.28, 5008.53, 5.40, 0.00, null, -5.40, + 992.31); + checkInst(model, 95, 95, LocalDate.of(2019, 4, 6), 106, 94, 50.00, 50.00, null, 0.90454700, 45.23, 4963.88, 5.35, 0.00, null, -5.35, + 992.31); + checkInst(model, 96, 96, LocalDate.of(2019, 4, 7), 105, 95, 50.00, 50.00, null, 0.90358215, 45.18, 4919.18, 5.30, 0.00, null, -5.30, + 992.31); + checkInst(model, 97, 97, LocalDate.of(2019, 4, 8), 104, 96, 50.00, 50.00, null, 0.90261832, 45.13, 4874.43, 5.25, 0.00, null, -5.25, + 992.31); + checkInst(model, 98, 98, LocalDate.of(2019, 4, 9), 103, 97, 50.00, 50.00, null, 0.90165552, 45.08, 4829.64, 5.20, 0.00, null, -5.20, + 992.31); + checkInst(model, 99, 99, LocalDate.of(2019, 4, 10), 102, 98, 50.00, 50.00, null, 0.90069374, 45.03, 4784.79, 5.16, 0.00, null, + -5.16, 992.31); + checkInst(model, 100, 100, LocalDate.of(2019, 4, 11), 101, 99, 50.00, 50.00, null, 0.89973299, 44.99, 4739.90, 5.11, 0.00, null, + -5.11, 992.31); + checkInst(model, 101, 101, LocalDate.of(2019, 4, 12), 100, 100, 50.00, 50.00, null, 0.89877327, 44.94, 4694.96, 5.06, 0.00, null, + -5.06, 992.31); + checkInst(model, 102, 102, LocalDate.of(2019, 4, 13), 99, 101, 50.00, 50.00, null, 0.89781457, 44.89, 4649.98, 5.01, 0.00, null, + -5.01, 992.31); + checkInst(model, 103, 103, LocalDate.of(2019, 4, 14), 98, 102, 50.00, 50.00, null, 0.89685689, 44.84, 4604.94, 4.97, 0.00, null, + -4.97, 992.31); + checkInst(model, 104, 104, LocalDate.of(2019, 4, 15), 97, 103, 50.00, 50.00, null, 0.89590024, 44.80, 4559.86, 4.92, 0.00, null, + -4.92, 992.31); + checkInst(model, 105, 105, LocalDate.of(2019, 4, 16), 96, 104, 50.00, 50.00, null, 0.89494460, 44.75, 4514.73, 4.87, 0.00, null, + -4.87, 992.31); + checkInst(model, 106, 106, LocalDate.of(2019, 4, 17), 95, 105, 50.00, 50.00, null, 0.89398999, 44.70, 4469.55, 4.82, 0.00, null, + -4.82, 992.31); + checkInst(model, 107, 107, LocalDate.of(2019, 4, 18), 94, 106, 50.00, 50.00, null, 0.89303639, 44.65, 4424.32, 4.77, 0.00, null, + -4.77, 992.31); + checkInst(model, 108, 108, LocalDate.of(2019, 4, 19), 93, 107, 50.00, 50.00, null, 0.89208381, 44.60, 4379.05, 4.72, 0.00, null, + -4.72, 992.31); + checkInst(model, 109, 109, LocalDate.of(2019, 4, 20), 92, 108, 50.00, 50.00, null, 0.89113225, 44.56, 4333.72, 4.68, 0.00, null, + -4.68, 992.31); + checkInst(model, 110, 110, LocalDate.of(2019, 4, 21), 91, 109, 50.00, 50.00, null, 0.89018170, 44.51, 4288.35, 4.63, 0.00, null, + -4.63, 992.31); + checkInst(model, 111, 111, LocalDate.of(2019, 4, 22), 90, 110, 50.00, 50.00, null, 0.88923216, 44.46, 4242.93, 4.58, 0.00, null, + -4.58, 992.31); + checkInst(model, 112, 112, LocalDate.of(2019, 4, 23), 89, 111, 50.00, 50.00, null, 0.88828364, 44.41, 4197.46, 4.53, 0.00, null, + -4.53, 992.31); + checkInst(model, 113, 113, LocalDate.of(2019, 4, 24), 88, 112, 50.00, 50.00, null, 0.88733613, 44.37, 4151.94, 4.48, 0.00, null, + -4.48, 992.31); + checkInst(model, 114, 114, LocalDate.of(2019, 4, 25), 87, 113, 50.00, 50.00, null, 0.88638963, 44.32, 4106.38, 4.43, 0.00, null, + -4.43, 992.31); + checkInst(model, 115, 115, LocalDate.of(2019, 4, 26), 86, 114, 50.00, 50.00, null, 0.88544414, 44.27, 4060.76, 4.38, 0.00, null, + -4.38, 992.31); + checkInst(model, 116, 116, LocalDate.of(2019, 4, 27), 85, 115, 50.00, 50.00, null, 0.88449966, 44.22, 4015.10, 4.34, 0.00, null, + -4.34, 992.31); + checkInst(model, 117, 117, LocalDate.of(2019, 4, 28), 84, 116, 50.00, 50.00, null, 0.88355619, 44.18, 3969.38, 4.29, 0.00, null, + -4.29, 992.31); + checkInst(model, 118, 118, LocalDate.of(2019, 4, 29), 83, 117, 50.00, 50.00, null, 0.88261372, 44.13, 3923.62, 4.24, 0.00, null, + -4.24, 992.31); + checkInst(model, 119, 119, LocalDate.of(2019, 4, 30), 82, 118, 50.00, 50.00, null, 0.88167226, 44.08, 3877.81, 4.19, 0.00, null, + -4.19, 992.31); + checkInst(model, 120, 120, LocalDate.of(2019, 5, 1), 81, 119, 50.00, 50.00, null, 0.88073180, 44.04, 3831.95, 4.14, 0.00, null, + -4.14, 992.31); + checkInst(model, 121, 121, LocalDate.of(2019, 5, 2), 80, 120, 50.00, 50.00, null, 0.87979234, 43.99, 3786.04, 4.09, 0.00, null, + -4.09, 992.31); + checkInst(model, 122, 122, LocalDate.of(2019, 5, 3), 79, 121, 50.00, 50.00, null, 0.87885389, 43.94, 3740.09, 4.04, 0.00, null, + -4.04, 992.31); + checkInst(model, 123, 123, LocalDate.of(2019, 5, 4), 78, 122, 50.00, 50.00, null, 0.87791644, 43.90, 3694.08, 3.99, 0.00, null, + -3.99, 992.31); + checkInst(model, 124, 124, LocalDate.of(2019, 5, 5), 77, 123, 50.00, 50.00, null, 0.87697999, 43.85, 3648.03, 3.94, 0.00, null, + -3.94, 992.31); + checkInst(model, 125, 125, LocalDate.of(2019, 5, 6), 76, 124, 50.00, 50.00, null, 0.87604453, 43.80, 3601.92, 3.90, 0.00, null, + -3.90, 992.31); + checkInst(model, 126, 126, LocalDate.of(2019, 5, 7), 75, 125, 50.00, 50.00, null, 0.87511008, 43.76, 3555.77, 3.85, 0.00, null, + -3.85, 992.31); + checkInst(model, 127, 127, LocalDate.of(2019, 5, 8), 74, 126, 50.00, 50.00, null, 0.87417662, 43.71, 3509.56, 3.80, 0.00, null, + -3.80, 992.31); + checkInst(model, 128, 128, LocalDate.of(2019, 5, 9), 73, 127, 50.00, 50.00, null, 0.87324416, 43.66, 3463.31, 3.75, 0.00, null, + -3.75, 992.31); + checkInst(model, 129, 129, LocalDate.of(2019, 5, 10), 72, 128, 50.00, 50.00, null, 0.87231269, 43.62, 3417.01, 3.70, 0.00, null, + -3.70, 992.31); + checkInst(model, 130, 130, LocalDate.of(2019, 5, 11), 71, 129, 50.00, 50.00, null, 0.87138221, 43.57, 3370.66, 3.65, 0.00, null, + -3.65, 992.31); + checkInst(model, 131, 131, LocalDate.of(2019, 5, 12), 70, 130, 50.00, 50.00, null, 0.87045273, 43.52, 3324.26, 3.60, 0.00, null, + -3.60, 992.31); + checkInst(model, 132, 132, LocalDate.of(2019, 5, 13), 69, 131, 50.00, 50.00, null, 0.86952424, 43.48, 3277.81, 3.55, 0.00, null, + -3.55, 992.31); + checkInst(model, 133, 133, LocalDate.of(2019, 5, 14), 68, 132, 50.00, 50.00, null, 0.86859674, 43.43, 3231.31, 3.50, 0.00, null, + -3.50, 992.31); + checkInst(model, 134, 134, LocalDate.of(2019, 5, 15), 67, 133, 50.00, 50.00, null, 0.86767023, 43.38, 3184.76, 3.45, 0.00, null, + -3.45, 992.31); + checkInst(model, 135, 135, LocalDate.of(2019, 5, 16), 66, 134, 50.00, 50.00, null, 0.86674471, 43.34, 3138.16, 3.40, 0.00, null, + -3.40, 992.31); + checkInst(model, 136, 136, LocalDate.of(2019, 5, 17), 65, 135, 50.00, 50.00, null, 0.86582017, 43.29, 3091.51, 3.35, 0.00, null, + -3.35, 992.31); + checkInst(model, 137, 137, LocalDate.of(2019, 5, 18), 64, 136, 50.00, 50.00, null, 0.86489662, 43.24, 3044.81, 3.30, 0.00, null, + -3.30, 992.31); + checkInst(model, 138, 138, LocalDate.of(2019, 5, 19), 63, 137, 50.00, 50.00, null, 0.86397406, 43.20, 2998.06, 3.25, 0.00, null, + -3.25, 992.31); + checkInst(model, 139, 139, LocalDate.of(2019, 5, 20), 62, 138, 50.00, 50.00, null, 0.86305248, 43.15, 2951.26, 3.20, 0.00, null, + -3.20, 992.31); + checkInst(model, 140, 140, LocalDate.of(2019, 5, 21), 61, 139, 50.00, 50.00, null, 0.86213188, 43.11, 2904.42, 3.15, 0.00, null, + -3.15, 992.31); + checkInst(model, 141, 141, LocalDate.of(2019, 5, 22), 60, 140, 50.00, 50.00, null, 0.86121227, 43.06, 2857.52, 3.10, 0.00, null, + -3.10, 992.31); + checkInst(model, 142, 142, LocalDate.of(2019, 5, 23), 59, 141, 50.00, 50.00, null, 0.86029363, 43.01, 2810.57, 3.05, 0.00, null, + -3.05, 992.31); + checkInst(model, 143, 143, LocalDate.of(2019, 5, 24), 58, 142, 50.00, 50.00, null, 0.85937598, 42.97, 2763.57, 3.00, 0.00, null, + -3.00, 992.31); + checkInst(model, 144, 144, LocalDate.of(2019, 5, 25), 57, 143, 50.00, 50.00, null, 0.85845930, 42.92, 2716.52, 2.95, 0.00, null, + -2.95, 992.31); + checkInst(model, 145, 145, LocalDate.of(2019, 5, 26), 56, 144, 50.00, 50.00, null, 0.85754361, 42.88, 2669.42, 2.90, 0.00, null, + -2.90, 992.31); + checkInst(model, 146, 146, LocalDate.of(2019, 5, 27), 55, 145, 50.00, 50.00, null, 0.85662889, 42.83, 2622.27, 2.85, 0.00, null, + -2.85, 992.31); + checkInst(model, 147, 147, LocalDate.of(2019, 5, 28), 54, 146, 50.00, 50.00, null, 0.85571514, 42.79, 2575.07, 2.80, 0.00, null, + -2.80, 992.31); + checkInst(model, 148, 148, LocalDate.of(2019, 5, 29), 53, 147, 50.00, 50.00, null, 0.85480237, 42.74, 2527.82, 2.75, 0.00, null, + -2.75, 992.31); + checkInst(model, 149, 149, LocalDate.of(2019, 5, 30), 52, 148, 50.00, 50.00, null, 0.85389057, 42.69, 2480.52, 2.70, 0.00, null, + -2.70, 992.31); + checkInst(model, 150, 150, LocalDate.of(2019, 5, 31), 51, 149, 50.00, 50.00, null, 0.85297975, 42.65, 2433.17, 2.65, 0.00, null, + -2.65, 992.31); + checkInst(model, 151, 151, LocalDate.of(2019, 6, 1), 50, 150, 50.00, 50.00, null, 0.85206990, 42.60, 2385.77, 2.60, 0.00, null, + -2.60, 992.31); + checkInst(model, 152, 152, LocalDate.of(2019, 6, 2), 49, 151, 50.00, 50.00, null, 0.85116101, 42.56, 2338.31, 2.55, 0.00, null, + -2.55, 992.31); + checkInst(model, 153, 153, LocalDate.of(2019, 6, 3), 48, 152, 50.00, 50.00, null, 0.85025310, 42.51, 2290.81, 2.50, 0.00, null, + -2.50, 992.31); + checkInst(model, 154, 154, LocalDate.of(2019, 6, 4), 47, 153, 50.00, 50.00, null, 0.84934616, 42.47, 2243.26, 2.45, 0.00, null, + -2.45, 992.31); + checkInst(model, 155, 155, LocalDate.of(2019, 6, 5), 46, 154, 50.00, 50.00, null, 0.84844018, 42.42, 2195.65, 2.40, 0.00, null, + -2.40, 992.31); + checkInst(model, 156, 156, LocalDate.of(2019, 6, 6), 45, 155, 50.00, 50.00, null, 0.84753517, 42.38, 2148.00, 2.34, 0.00, null, + -2.34, 992.31); + checkInst(model, 157, 157, LocalDate.of(2019, 6, 7), 44, 156, 50.00, 50.00, null, 0.84663113, 42.33, 2100.29, 2.29, 0.00, null, + -2.29, 992.31); + checkInst(model, 158, 158, LocalDate.of(2019, 6, 8), 43, 157, 50.00, 50.00, null, 0.84572805, 42.29, 2052.53, 2.24, 0.00, null, + -2.24, 992.31); + checkInst(model, 159, 159, LocalDate.of(2019, 6, 9), 42, 158, 50.00, 50.00, null, 0.84482593, 42.24, 2004.73, 2.19, 0.00, null, + -2.19, 992.31); + checkInst(model, 160, 160, LocalDate.of(2019, 6, 10), 41, 159, 50.00, 50.00, null, 0.84392477, 42.20, 1956.87, 2.14, 0.00, null, + -2.14, 992.31); + checkInst(model, 161, 161, LocalDate.of(2019, 6, 11), 40, 160, 50.00, 50.00, null, 0.84302458, 42.15, 1908.96, 2.09, 0.00, null, + -2.09, 992.31); + checkInst(model, 162, 162, LocalDate.of(2019, 6, 12), 39, 161, 50.00, 50.00, null, 0.84212535, 42.11, 1860.99, 2.04, 0.00, null, + -2.04, 992.31); + checkInst(model, 163, 163, LocalDate.of(2019, 6, 13), 38, 162, 50.00, 50.00, null, 0.84122707, 42.06, 1812.98, 1.99, 0.00, null, + -1.99, 992.31); + checkInst(model, 164, 164, LocalDate.of(2019, 6, 14), 37, 163, 50.00, 50.00, null, 0.84032975, 42.02, 1764.92, 1.94, 0.00, null, + -1.94, 992.31); + checkInst(model, 165, 165, LocalDate.of(2019, 6, 15), 36, 164, 50.00, 50.00, null, 0.83943340, 41.97, 1716.80, 1.88, 0.00, null, + -1.88, 992.31); + checkInst(model, 166, 166, LocalDate.of(2019, 6, 16), 35, 165, 50.00, 50.00, null, 0.83853799, 41.93, 1668.64, 1.83, 0.00, null, + -1.83, 992.31); + checkInst(model, 167, 167, LocalDate.of(2019, 6, 17), 34, 166, 50.00, 50.00, null, 0.83764354, 41.88, 1620.42, 1.78, 0.00, null, + -1.78, 992.31); + checkInst(model, 168, 168, LocalDate.of(2019, 6, 18), 33, 167, 50.00, 50.00, null, 0.83675005, 41.84, 1572.15, 1.73, 0.00, null, + -1.73, 992.31); + checkInst(model, 169, 169, LocalDate.of(2019, 6, 19), 32, 168, 50.00, 50.00, null, 0.83585751, 41.79, 1523.83, 1.68, 0.00, null, + -1.68, 992.31); + checkInst(model, 170, 170, LocalDate.of(2019, 6, 20), 31, 169, 50.00, 50.00, null, 0.83496592, 41.75, 1475.45, 1.63, 0.00, null, + -1.63, 992.31); + checkInst(model, 171, 171, LocalDate.of(2019, 6, 21), 30, 170, 50.00, 50.00, null, 0.83407528, 41.70, 1427.03, 1.58, 0.00, null, + -1.58, 992.31); + checkInst(model, 172, 172, LocalDate.of(2019, 6, 22), 29, 171, 50.00, 50.00, null, 0.83318560, 41.66, 1378.55, 1.52, 0.00, null, + -1.52, 992.31); + checkInst(model, 173, 173, LocalDate.of(2019, 6, 23), 28, 172, 50.00, 50.00, null, 0.83229686, 41.61, 1330.02, 1.47, 0.00, null, + -1.47, 992.31); + checkInst(model, 174, 174, LocalDate.of(2019, 6, 24), 27, 173, 50.00, 50.00, null, 0.83140907, 41.57, 1281.45, 1.42, 0.00, null, + -1.42, 992.31); + checkInst(model, 175, 175, LocalDate.of(2019, 6, 25), 26, 174, 50.00, 50.00, null, 0.83052222, 41.53, 1232.81, 1.37, 0.00, null, + -1.37, 992.31); + checkInst(model, 176, 176, LocalDate.of(2019, 6, 26), 25, 175, 50.00, 50.00, null, 0.82963633, 41.48, 1184.13, 1.32, 0.00, null, + -1.32, 992.31); + checkInst(model, 177, 177, LocalDate.of(2019, 6, 27), 24, 176, 50.00, 50.00, null, 0.82875137, 41.44, 1135.39, 1.26, 0.00, null, + -1.26, 992.31); + checkInst(model, 178, 178, LocalDate.of(2019, 6, 28), 23, 177, 50.00, 50.00, null, 0.82786736, 41.39, 1086.61, 1.21, 0.00, null, + -1.21, 992.31); + checkInst(model, 179, 179, LocalDate.of(2019, 6, 29), 22, 178, 50.00, 50.00, null, 0.82698430, 41.35, 1037.77, 1.16, 0.00, null, + -1.16, 992.31); + checkInst(model, 180, 180, LocalDate.of(2019, 6, 30), 21, 179, 50.00, 50.00, null, 0.82610217, 41.31, 988.88, 1.11, 0.00, null, + -1.11, 992.31); + checkInst(model, 181, 181, LocalDate.of(2019, 7, 1), 20, 180, 50.00, 50.00, null, 0.82522099, 41.26, 939.93, 1.06, 0.00, null, + -1.06, 992.31); + checkInst(model, 182, 182, LocalDate.of(2019, 7, 2), 19, 181, 50.00, 50.00, null, 0.82434075, 41.22, 890.93, 1.00, 0.00, null, + -1.00, 992.31); + checkInst(model, 183, 183, LocalDate.of(2019, 7, 3), 18, 182, 50.00, 50.00, null, 0.82346144, 41.17, 841.89, 0.95, 0.00, null, + -0.95, 992.31); + checkInst(model, 184, 184, LocalDate.of(2019, 7, 4), 17, 183, 50.00, 50.00, null, 0.82258308, 41.13, 792.79, 0.90, 0.00, null, + -0.90, 992.31); + checkInst(model, 185, 185, LocalDate.of(2019, 7, 5), 16, 184, 50.00, 50.00, null, 0.82170565, 41.09, 743.63, 0.85, 0.00, null, + -0.85, 992.31); + checkInst(model, 186, 186, LocalDate.of(2019, 7, 6), 15, 185, 50.00, 50.00, null, 0.82082916, 41.04, 694.43, 0.79, 0.00, null, + -0.79, 992.31); + checkInst(model, 187, 187, LocalDate.of(2019, 7, 7), 14, 186, 50.00, 50.00, null, 0.81995360, 41.00, 645.17, 0.74, 0.00, null, + -0.74, 992.31); + checkInst(model, 188, 188, LocalDate.of(2019, 7, 8), 13, 187, 50.00, 50.00, null, 0.81907897, 40.95, 595.86, 0.69, 0.00, null, + -0.69, 992.31); + checkInst(model, 189, 189, LocalDate.of(2019, 7, 9), 12, 188, 50.00, 50.00, null, 0.81820528, 40.91, 546.49, 0.64, 0.00, null, + -0.64, 992.31); + checkInst(model, 190, 190, LocalDate.of(2019, 7, 10), 11, 189, 50.00, 50.00, null, 0.81733252, 40.87, 497.08, 0.58, 0.00, null, + -0.58, 992.31); + checkInst(model, 191, 191, LocalDate.of(2019, 7, 11), 10, 190, 50.00, 50.00, null, 0.81646069, 40.82, 447.61, 0.53, 0.00, null, + -0.53, 992.31); + checkInst(model, 192, 192, LocalDate.of(2019, 7, 12), 9, 191, 50.00, 50.00, null, 0.81558979, 40.78, 398.08, 0.48, 0.00, null, + -0.48, 992.31); + checkInst(model, 193, 193, LocalDate.of(2019, 7, 13), 8, 192, 50.00, 50.00, null, 0.81471983, 40.74, 348.51, 0.43, 0.00, null, + -0.43, 992.31); + checkInst(model, 194, 194, LocalDate.of(2019, 7, 14), 7, 193, 50.00, 50.00, null, 0.81385078, 40.69, 298.88, 0.37, 0.00, null, + -0.37, 992.31); + checkInst(model, 195, 195, LocalDate.of(2019, 7, 15), 6, 194, 50.00, 50.00, null, 0.81298267, 40.65, 249.20, 0.32, 0.00, null, + -0.32, 992.31); + checkInst(model, 196, 196, LocalDate.of(2019, 7, 16), 5, 195, 50.00, 50.00, null, 0.81211548, 40.61, 199.47, 0.27, 0.00, null, + -0.27, 992.31); + checkInst(model, 197, 197, LocalDate.of(2019, 7, 17), 4, 196, 50.00, 50.00, null, 0.81124922, 40.56, 149.68, 0.21, 0.00, null, + -0.21, 992.31); + checkInst(model, 198, 198, LocalDate.of(2019, 7, 18), 3, 197, 50.00, 50.00, null, 0.81038388, 40.52, 99.84, 0.16, 0.00, null, -0.16, + 992.31); + checkInst(model, 199, 199, LocalDate.of(2019, 7, 19), 2, 198, 50.00, 50.00, null, 0.80951946, 40.48, 49.95, 0.11, 0.00, null, -0.11, + 992.31); + checkInst(model, 200, 200, LocalDate.of(2019, 7, 20), 1, 199, 50.00, 50.00, null, 0.80865597, 40.43, 0.00, 0.05, 0.00, null, -0.05, + 992.31); + + assertEquals(202, model.payments().size(), "disbursement + 200 regular + 1 additional"); + checkInst(model, 201, 201, LocalDate.of(2019, 7, 21), 0, 200, null, 10.00, null, 0.80779339, 8.08, null, null, 0.00, null, null, + null); + } + + @Test + void testNoPayment_term200_originationFee1000_netDisbursement9000_pay0_0_50() { + final ProjectedAmortizationScheduleModel model = generateModel(); + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(1), BigDecimal.ZERO); + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(2), BigDecimal.ZERO); + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(3), new BigDecimal("50")); + + checkInst(model, 0, 0, EXPECTED_DISBURSEMENT_DATE, 203, 0, -9000.00, null, null, 1.00000000, -9000.00, 9000.00, null, null, null, + null, 1000.00); + + checkInst(model, 1, 1, LocalDate.of(2019, 1, 2), 202, 0, 50.00, 50.00, 0.00, 1.00000000, 0.00, 8959.61, 9.61, 9.61, 0.00, -9.61, + 1000.00); + checkInst(model, 2, 2, LocalDate.of(2019, 1, 3), 201, 0, 50.00, 50.00, 0.00, 1.00000000, 0.00, 8919.18, 9.57, 9.61, 0.00, -9.57, + 1000.00); + checkInst(model, 3, 3, LocalDate.of(2019, 1, 4), 200, 0, 50.00, 50.00, 50.00, 1.00000000, 50.00, 8878.70, 9.52, 9.61, 9.61, 0.09, + 990.39); + + checkInst(model, 4, 4, LocalDate.of(2019, 1, 5), 199, 1, 50.00, 50.00, null, 0.99893332, 49.95, 8838.18, 9.48, 0.00, null, -9.48, + 990.39); + checkInst(model, 5, 5, LocalDate.of(2019, 1, 6), 198, 2, 50.00, 50.00, null, 0.99786779, 49.89, 8797.62, 9.44, 0.00, null, -9.44, + 990.39); + checkInst(model, 6, 6, LocalDate.of(2019, 1, 7), 197, 3, 50.00, 50.00, null, 0.99680339, 49.84, 8757.01, 9.39, 0.00, null, -9.39, + 990.39); + checkInst(model, 7, 7, LocalDate.of(2019, 1, 8), 196, 4, 50.00, 50.00, null, 0.99574012, 49.79, 8716.36, 9.35, 0.00, null, -9.35, + 990.39); + checkInst(model, 8, 8, LocalDate.of(2019, 1, 9), 195, 5, 50.00, 50.00, null, 0.99467799, 49.73, 8675.67, 9.31, 0.00, null, -9.31, + 990.39); + checkInst(model, 9, 9, LocalDate.of(2019, 1, 10), 194, 6, 50.00, 50.00, null, 0.99361699, 49.68, 8634.94, 9.26, 0.00, null, -9.26, + 990.39); + checkInst(model, 10, 10, LocalDate.of(2019, 1, 11), 193, 7, 50.00, 50.00, null, 0.99255712, 49.63, 8594.16, 9.22, 0.00, null, -9.22, + 990.39); + checkInst(model, 11, 11, LocalDate.of(2019, 1, 12), 192, 8, 50.00, 50.00, null, 0.99149839, 49.57, 8553.33, 9.18, 0.00, null, -9.18, + 990.39); + checkInst(model, 12, 12, LocalDate.of(2019, 1, 13), 191, 9, 50.00, 50.00, null, 0.99044078, 49.52, 8512.47, 9.13, 0.00, null, -9.13, + 990.39); + checkInst(model, 13, 13, LocalDate.of(2019, 1, 14), 190, 10, 50.00, 50.00, null, 0.98938430, 49.47, 8471.56, 9.09, 0.00, null, + -9.09, 990.39); + checkInst(model, 14, 14, LocalDate.of(2019, 1, 15), 189, 11, 50.00, 50.00, null, 0.98832895, 49.42, 8430.60, 9.05, 0.00, null, + -9.05, 990.39); + checkInst(model, 15, 15, LocalDate.of(2019, 1, 16), 188, 12, 50.00, 50.00, null, 0.98727472, 49.36, 8389.61, 9.00, 0.00, null, + -9.00, 990.39); + checkInst(model, 16, 16, LocalDate.of(2019, 1, 17), 187, 13, 50.00, 50.00, null, 0.98622162, 49.31, 8348.56, 8.96, 0.00, null, + -8.96, 990.39); + checkInst(model, 17, 17, LocalDate.of(2019, 1, 18), 186, 14, 50.00, 50.00, null, 0.98516964, 49.26, 8307.48, 8.91, 0.00, null, + -8.91, 990.39); + checkInst(model, 18, 18, LocalDate.of(2019, 1, 19), 185, 15, 50.00, 50.00, null, 0.98411879, 49.21, 8266.35, 8.87, 0.00, null, + -8.87, 990.39); + checkInst(model, 19, 19, LocalDate.of(2019, 1, 20), 184, 16, 50.00, 50.00, null, 0.98306905, 49.15, 8225.18, 8.83, 0.00, null, + -8.83, 990.39); + checkInst(model, 20, 20, LocalDate.of(2019, 1, 21), 183, 17, 50.00, 50.00, null, 0.98202044, 49.10, 8183.96, 8.78, 0.00, null, + -8.78, 990.39); + checkInst(model, 21, 21, LocalDate.of(2019, 1, 22), 182, 18, 50.00, 50.00, null, 0.98097294, 49.05, 8142.70, 8.74, 0.00, null, + -8.74, 990.39); + checkInst(model, 22, 22, LocalDate.of(2019, 1, 23), 181, 19, 50.00, 50.00, null, 0.97992656, 49.00, 8101.39, 8.69, 0.00, null, + -8.69, 990.39); + checkInst(model, 23, 23, LocalDate.of(2019, 1, 24), 180, 20, 50.00, 50.00, null, 0.97888129, 48.94, 8060.04, 8.65, 0.00, null, + -8.65, 990.39); + checkInst(model, 24, 24, LocalDate.of(2019, 1, 25), 179, 21, 50.00, 50.00, null, 0.97783715, 48.89, 8018.65, 8.61, 0.00, null, + -8.61, 990.39); + checkInst(model, 25, 25, LocalDate.of(2019, 1, 26), 178, 22, 50.00, 50.00, null, 0.97679411, 48.84, 7977.21, 8.56, 0.00, null, + -8.56, 990.39); + checkInst(model, 26, 26, LocalDate.of(2019, 1, 27), 177, 23, 50.00, 50.00, null, 0.97575219, 48.79, 7935.73, 8.52, 0.00, null, + -8.52, 990.39); + checkInst(model, 27, 27, LocalDate.of(2019, 1, 28), 176, 24, 50.00, 50.00, null, 0.97471138, 48.74, 7894.21, 8.47, 0.00, null, + -8.47, 990.39); + checkInst(model, 28, 28, LocalDate.of(2019, 1, 29), 175, 25, 50.00, 50.00, null, 0.97367168, 48.68, 7852.63, 8.43, 0.00, null, + -8.43, 990.39); + checkInst(model, 29, 29, LocalDate.of(2019, 1, 30), 174, 26, 50.00, 50.00, null, 0.97263309, 48.63, 7811.02, 8.39, 0.00, null, + -8.39, 990.39); + checkInst(model, 30, 30, LocalDate.of(2019, 1, 31), 173, 27, 50.00, 50.00, null, 0.97159560, 48.58, 7769.36, 8.34, 0.00, null, + -8.34, 990.39); + checkInst(model, 31, 31, LocalDate.of(2019, 2, 1), 172, 28, 50.00, 50.00, null, 0.97055922, 48.53, 7727.66, 8.30, 0.00, null, -8.30, + 990.39); + checkInst(model, 32, 32, LocalDate.of(2019, 2, 2), 171, 29, 50.00, 50.00, null, 0.96952395, 48.48, 7685.91, 8.25, 0.00, null, -8.25, + 990.39); + checkInst(model, 33, 33, LocalDate.of(2019, 2, 3), 170, 30, 50.00, 50.00, null, 0.96848979, 48.42, 7644.12, 8.21, 0.00, null, -8.21, + 990.39); + checkInst(model, 34, 34, LocalDate.of(2019, 2, 4), 169, 31, 50.00, 50.00, null, 0.96745672, 48.37, 7602.28, 8.16, 0.00, null, -8.16, + 990.39); + checkInst(model, 35, 35, LocalDate.of(2019, 2, 5), 168, 32, 50.00, 50.00, null, 0.96642476, 48.32, 7560.40, 8.12, 0.00, null, -8.12, + 990.39); + checkInst(model, 36, 36, LocalDate.of(2019, 2, 6), 167, 33, 50.00, 50.00, null, 0.96539390, 48.27, 7518.47, 8.07, 0.00, null, -8.07, + 990.39); + checkInst(model, 37, 37, LocalDate.of(2019, 2, 7), 166, 34, 50.00, 50.00, null, 0.96436413, 48.22, 7476.50, 8.03, 0.00, null, -8.03, + 990.39); + checkInst(model, 38, 38, LocalDate.of(2019, 2, 8), 165, 35, 50.00, 50.00, null, 0.96333547, 48.17, 7434.48, 7.98, 0.00, null, -7.98, + 990.39); + checkInst(model, 39, 39, LocalDate.of(2019, 2, 9), 164, 36, 50.00, 50.00, null, 0.96230790, 48.12, 7392.42, 7.94, 0.00, null, -7.94, + 990.39); + checkInst(model, 40, 40, LocalDate.of(2019, 2, 10), 163, 37, 50.00, 50.00, null, 0.96128143, 48.06, 7350.31, 7.89, 0.00, null, + -7.89, 990.39); + checkInst(model, 41, 41, LocalDate.of(2019, 2, 11), 162, 38, 50.00, 50.00, null, 0.96025606, 48.01, 7308.16, 7.85, 0.00, null, + -7.85, 990.39); + checkInst(model, 42, 42, LocalDate.of(2019, 2, 12), 161, 39, 50.00, 50.00, null, 0.95923178, 47.96, 7265.97, 7.80, 0.00, null, + -7.80, 990.39); + checkInst(model, 43, 43, LocalDate.of(2019, 2, 13), 160, 40, 50.00, 50.00, null, 0.95820859, 47.91, 7223.72, 7.76, 0.00, null, + -7.76, 990.39); + checkInst(model, 44, 44, LocalDate.of(2019, 2, 14), 159, 41, 50.00, 50.00, null, 0.95718649, 47.86, 7181.44, 7.71, 0.00, null, + -7.71, 990.39); + checkInst(model, 45, 45, LocalDate.of(2019, 2, 15), 158, 42, 50.00, 50.00, null, 0.95616548, 47.81, 7139.11, 7.67, 0.00, null, + -7.67, 990.39); + checkInst(model, 46, 46, LocalDate.of(2019, 2, 16), 157, 43, 50.00, 50.00, null, 0.95514557, 47.76, 7096.73, 7.62, 0.00, null, + -7.62, 990.39); + checkInst(model, 47, 47, LocalDate.of(2019, 2, 17), 156, 44, 50.00, 50.00, null, 0.95412674, 47.71, 7054.31, 7.58, 0.00, null, + -7.58, 990.39); + checkInst(model, 48, 48, LocalDate.of(2019, 2, 18), 155, 45, 50.00, 50.00, null, 0.95310899, 47.66, 7011.84, 7.53, 0.00, null, + -7.53, 990.39); + checkInst(model, 49, 49, LocalDate.of(2019, 2, 19), 154, 46, 50.00, 50.00, null, 0.95209233, 47.60, 6969.33, 7.49, 0.00, null, + -7.49, 990.39); + checkInst(model, 50, 50, LocalDate.of(2019, 2, 20), 153, 47, 50.00, 50.00, null, 0.95107676, 47.55, 6926.77, 7.44, 0.00, null, + -7.44, 990.39); + checkInst(model, 51, 51, LocalDate.of(2019, 2, 21), 152, 48, 50.00, 50.00, null, 0.95006227, 47.50, 6884.17, 7.40, 0.00, null, + -7.40, 990.39); + checkInst(model, 52, 52, LocalDate.of(2019, 2, 22), 151, 49, 50.00, 50.00, null, 0.94904886, 47.45, 6841.52, 7.35, 0.00, null, + -7.35, 990.39); + checkInst(model, 53, 53, LocalDate.of(2019, 2, 23), 150, 50, 50.00, 50.00, null, 0.94803653, 47.40, 6798.82, 7.31, 0.00, null, + -7.31, 990.39); + checkInst(model, 54, 54, LocalDate.of(2019, 2, 24), 149, 51, 50.00, 50.00, null, 0.94702529, 47.35, 6756.08, 7.26, 0.00, null, + -7.26, 990.39); + checkInst(model, 55, 55, LocalDate.of(2019, 2, 25), 148, 52, 50.00, 50.00, null, 0.94601512, 47.30, 6713.30, 7.21, 0.00, null, + -7.21, 990.39); + checkInst(model, 56, 56, LocalDate.of(2019, 2, 26), 147, 53, 50.00, 50.00, null, 0.94500603, 47.25, 6670.47, 7.17, 0.00, null, + -7.17, 990.39); + checkInst(model, 57, 57, LocalDate.of(2019, 2, 27), 146, 54, 50.00, 50.00, null, 0.94399801, 47.20, 6627.59, 7.12, 0.00, null, + -7.12, 990.39); + checkInst(model, 58, 58, LocalDate.of(2019, 2, 28), 145, 55, 50.00, 50.00, null, 0.94299107, 47.15, 6584.67, 7.08, 0.00, null, + -7.08, 990.39); + checkInst(model, 59, 59, LocalDate.of(2019, 3, 1), 144, 56, 50.00, 50.00, null, 0.94198521, 47.10, 6541.70, 7.03, 0.00, null, -7.03, + 990.39); + checkInst(model, 60, 60, LocalDate.of(2019, 3, 2), 143, 57, 50.00, 50.00, null, 0.94098042, 47.05, 6498.68, 6.99, 0.00, null, -6.99, + 990.39); + checkInst(model, 61, 61, LocalDate.of(2019, 3, 3), 142, 58, 50.00, 50.00, null, 0.93997669, 47.00, 6455.62, 6.94, 0.00, null, -6.94, + 990.39); + checkInst(model, 62, 62, LocalDate.of(2019, 3, 4), 141, 59, 50.00, 50.00, null, 0.93897404, 46.95, 6412.51, 6.89, 0.00, null, -6.89, + 990.39); + checkInst(model, 63, 63, LocalDate.of(2019, 3, 5), 140, 60, 50.00, 50.00, null, 0.93797246, 46.90, 6369.36, 6.85, 0.00, null, -6.85, + 990.39); + checkInst(model, 64, 64, LocalDate.of(2019, 3, 6), 139, 61, 50.00, 50.00, null, 0.93697195, 46.85, 6326.16, 6.80, 0.00, null, -6.80, + 990.39); + checkInst(model, 65, 65, LocalDate.of(2019, 3, 7), 138, 62, 50.00, 50.00, null, 0.93597251, 46.80, 6282.92, 6.76, 0.00, null, -6.76, + 990.39); + checkInst(model, 66, 66, LocalDate.of(2019, 3, 8), 137, 63, 50.00, 50.00, null, 0.93497413, 46.75, 6239.63, 6.71, 0.00, null, -6.71, + 990.39); + checkInst(model, 67, 67, LocalDate.of(2019, 3, 9), 136, 64, 50.00, 50.00, null, 0.93397681, 46.70, 6196.29, 6.66, 0.00, null, -6.66, + 990.39); + checkInst(model, 68, 68, LocalDate.of(2019, 3, 10), 135, 65, 50.00, 50.00, null, 0.93298056, 46.65, 6152.91, 6.62, 0.00, null, + -6.62, 990.39); + checkInst(model, 69, 69, LocalDate.of(2019, 3, 11), 134, 66, 50.00, 50.00, null, 0.93198538, 46.60, 6109.48, 6.57, 0.00, null, + -6.57, 990.39); + checkInst(model, 70, 70, LocalDate.of(2019, 3, 12), 133, 67, 50.00, 50.00, null, 0.93099125, 46.55, 6066.00, 6.52, 0.00, null, + -6.52, 990.39); + checkInst(model, 71, 71, LocalDate.of(2019, 3, 13), 132, 68, 50.00, 50.00, null, 0.92999818, 46.50, 6022.48, 6.48, 0.00, null, + -6.48, 990.39); + checkInst(model, 72, 72, LocalDate.of(2019, 3, 14), 131, 69, 50.00, 50.00, null, 0.92900618, 46.45, 5978.91, 6.43, 0.00, null, + -6.43, 990.39); + checkInst(model, 73, 73, LocalDate.of(2019, 3, 15), 130, 70, 50.00, 50.00, null, 0.92801523, 46.40, 5935.29, 6.38, 0.00, null, + -6.38, 990.39); + checkInst(model, 74, 74, LocalDate.of(2019, 3, 16), 129, 71, 50.00, 50.00, null, 0.92702534, 46.35, 5891.63, 6.34, 0.00, null, + -6.34, 990.39); + checkInst(model, 75, 75, LocalDate.of(2019, 3, 17), 128, 72, 50.00, 50.00, null, 0.92603650, 46.30, 5847.92, 6.29, 0.00, null, + -6.29, 990.39); + checkInst(model, 76, 76, LocalDate.of(2019, 3, 18), 127, 73, 50.00, 50.00, null, 0.92504872, 46.25, 5804.17, 6.24, 0.00, null, + -6.24, 990.39); + checkInst(model, 77, 77, LocalDate.of(2019, 3, 19), 126, 74, 50.00, 50.00, null, 0.92406200, 46.20, 5760.36, 6.20, 0.00, null, + -6.20, 990.39); + checkInst(model, 78, 78, LocalDate.of(2019, 3, 20), 125, 75, 50.00, 50.00, null, 0.92307632, 46.15, 5716.52, 6.15, 0.00, null, + -6.15, 990.39); + checkInst(model, 79, 79, LocalDate.of(2019, 3, 21), 124, 76, 50.00, 50.00, null, 0.92209170, 46.10, 5672.62, 6.10, 0.00, null, + -6.10, 990.39); + checkInst(model, 80, 80, LocalDate.of(2019, 3, 22), 123, 77, 50.00, 50.00, null, 0.92110813, 46.06, 5628.68, 6.06, 0.00, null, + -6.06, 990.39); + checkInst(model, 81, 81, LocalDate.of(2019, 3, 23), 122, 78, 50.00, 50.00, null, 0.92012560, 46.01, 5584.69, 6.01, 0.00, null, + -6.01, 990.39); + checkInst(model, 82, 82, LocalDate.of(2019, 3, 24), 121, 79, 50.00, 50.00, null, 0.91914413, 45.96, 5540.65, 5.96, 0.00, null, + -5.96, 990.39); + checkInst(model, 83, 83, LocalDate.of(2019, 3, 25), 120, 80, 50.00, 50.00, null, 0.91816370, 45.91, 5496.57, 5.92, 0.00, null, + -5.92, 990.39); + checkInst(model, 84, 84, LocalDate.of(2019, 3, 26), 119, 81, 50.00, 50.00, null, 0.91718432, 45.86, 5452.44, 5.87, 0.00, null, + -5.87, 990.39); + checkInst(model, 85, 85, LocalDate.of(2019, 3, 27), 118, 82, 50.00, 50.00, null, 0.91620598, 45.81, 5408.26, 5.82, 0.00, null, + -5.82, 990.39); + checkInst(model, 86, 86, LocalDate.of(2019, 3, 28), 117, 83, 50.00, 50.00, null, 0.91522868, 45.76, 5364.03, 5.78, 0.00, null, + -5.78, 990.39); + checkInst(model, 87, 87, LocalDate.of(2019, 3, 29), 116, 84, 50.00, 50.00, null, 0.91425243, 45.71, 5319.76, 5.73, 0.00, null, + -5.73, 990.39); + checkInst(model, 88, 88, LocalDate.of(2019, 3, 30), 115, 85, 50.00, 50.00, null, 0.91327722, 45.66, 5275.44, 5.68, 0.00, null, + -5.68, 990.39); + checkInst(model, 89, 89, LocalDate.of(2019, 3, 31), 114, 86, 50.00, 50.00, null, 0.91230305, 45.62, 5231.08, 5.63, 0.00, null, + -5.63, 990.39); + checkInst(model, 90, 90, LocalDate.of(2019, 4, 1), 113, 87, 50.00, 50.00, null, 0.91132992, 45.57, 5186.66, 5.59, 0.00, null, -5.59, + 990.39); + checkInst(model, 91, 91, LocalDate.of(2019, 4, 2), 112, 88, 50.00, 50.00, null, 0.91035783, 45.52, 5142.20, 5.54, 0.00, null, -5.54, + 990.39); + checkInst(model, 92, 92, LocalDate.of(2019, 4, 3), 111, 89, 50.00, 50.00, null, 0.90938677, 45.47, 5097.69, 5.49, 0.00, null, -5.49, + 990.39); + checkInst(model, 93, 93, LocalDate.of(2019, 4, 4), 110, 90, 50.00, 50.00, null, 0.90841675, 45.42, 5053.13, 5.44, 0.00, null, -5.44, + 990.39); + checkInst(model, 94, 94, LocalDate.of(2019, 4, 5), 109, 91, 50.00, 50.00, null, 0.90744776, 45.37, 5008.53, 5.40, 0.00, null, -5.40, + 990.39); + checkInst(model, 95, 95, LocalDate.of(2019, 4, 6), 108, 92, 50.00, 50.00, null, 0.90647981, 45.32, 4963.88, 5.35, 0.00, null, -5.35, + 990.39); + checkInst(model, 96, 96, LocalDate.of(2019, 4, 7), 107, 93, 50.00, 50.00, null, 0.90551289, 45.28, 4919.18, 5.30, 0.00, null, -5.30, + 990.39); + checkInst(model, 97, 97, LocalDate.of(2019, 4, 8), 106, 94, 50.00, 50.00, null, 0.90454700, 45.23, 4874.43, 5.25, 0.00, null, -5.25, + 990.39); + checkInst(model, 98, 98, LocalDate.of(2019, 4, 9), 105, 95, 50.00, 50.00, null, 0.90358215, 45.18, 4829.64, 5.20, 0.00, null, -5.20, + 990.39); + checkInst(model, 99, 99, LocalDate.of(2019, 4, 10), 104, 96, 50.00, 50.00, null, 0.90261832, 45.13, 4784.79, 5.16, 0.00, null, + -5.16, 990.39); + checkInst(model, 100, 100, LocalDate.of(2019, 4, 11), 103, 97, 50.00, 50.00, null, 0.90165552, 45.08, 4739.90, 5.11, 0.00, null, + -5.11, 990.39); + checkInst(model, 101, 101, LocalDate.of(2019, 4, 12), 102, 98, 50.00, 50.00, null, 0.90069374, 45.03, 4694.96, 5.06, 0.00, null, + -5.06, 990.39); + checkInst(model, 102, 102, LocalDate.of(2019, 4, 13), 101, 99, 50.00, 50.00, null, 0.89973299, 44.99, 4649.98, 5.01, 0.00, null, + -5.01, 990.39); + checkInst(model, 103, 103, LocalDate.of(2019, 4, 14), 100, 100, 50.00, 50.00, null, 0.89877327, 44.94, 4604.94, 4.97, 0.00, null, + -4.97, 990.39); + checkInst(model, 104, 104, LocalDate.of(2019, 4, 15), 99, 101, 50.00, 50.00, null, 0.89781457, 44.89, 4559.86, 4.92, 0.00, null, + -4.92, 990.39); + checkInst(model, 105, 105, LocalDate.of(2019, 4, 16), 98, 102, 50.00, 50.00, null, 0.89685689, 44.84, 4514.73, 4.87, 0.00, null, + -4.87, 990.39); + checkInst(model, 106, 106, LocalDate.of(2019, 4, 17), 97, 103, 50.00, 50.00, null, 0.89590024, 44.80, 4469.55, 4.82, 0.00, null, + -4.82, 990.39); + checkInst(model, 107, 107, LocalDate.of(2019, 4, 18), 96, 104, 50.00, 50.00, null, 0.89494460, 44.75, 4424.32, 4.77, 0.00, null, + -4.77, 990.39); + checkInst(model, 108, 108, LocalDate.of(2019, 4, 19), 95, 105, 50.00, 50.00, null, 0.89398999, 44.70, 4379.05, 4.72, 0.00, null, + -4.72, 990.39); + checkInst(model, 109, 109, LocalDate.of(2019, 4, 20), 94, 106, 50.00, 50.00, null, 0.89303639, 44.65, 4333.72, 4.68, 0.00, null, + -4.68, 990.39); + checkInst(model, 110, 110, LocalDate.of(2019, 4, 21), 93, 107, 50.00, 50.00, null, 0.89208381, 44.60, 4288.35, 4.63, 0.00, null, + -4.63, 990.39); + checkInst(model, 111, 111, LocalDate.of(2019, 4, 22), 92, 108, 50.00, 50.00, null, 0.89113225, 44.56, 4242.93, 4.58, 0.00, null, + -4.58, 990.39); + checkInst(model, 112, 112, LocalDate.of(2019, 4, 23), 91, 109, 50.00, 50.00, null, 0.89018170, 44.51, 4197.46, 4.53, 0.00, null, + -4.53, 990.39); + checkInst(model, 113, 113, LocalDate.of(2019, 4, 24), 90, 110, 50.00, 50.00, null, 0.88923216, 44.46, 4151.94, 4.48, 0.00, null, + -4.48, 990.39); + checkInst(model, 114, 114, LocalDate.of(2019, 4, 25), 89, 111, 50.00, 50.00, null, 0.88828364, 44.41, 4106.38, 4.43, 0.00, null, + -4.43, 990.39); + checkInst(model, 115, 115, LocalDate.of(2019, 4, 26), 88, 112, 50.00, 50.00, null, 0.88733613, 44.37, 4060.76, 4.38, 0.00, null, + -4.38, 990.39); + checkInst(model, 116, 116, LocalDate.of(2019, 4, 27), 87, 113, 50.00, 50.00, null, 0.88638963, 44.32, 4015.10, 4.34, 0.00, null, + -4.34, 990.39); + checkInst(model, 117, 117, LocalDate.of(2019, 4, 28), 86, 114, 50.00, 50.00, null, 0.88544414, 44.27, 3969.38, 4.29, 0.00, null, + -4.29, 990.39); + checkInst(model, 118, 118, LocalDate.of(2019, 4, 29), 85, 115, 50.00, 50.00, null, 0.88449966, 44.22, 3923.62, 4.24, 0.00, null, + -4.24, 990.39); + checkInst(model, 119, 119, LocalDate.of(2019, 4, 30), 84, 116, 50.00, 50.00, null, 0.88355619, 44.18, 3877.81, 4.19, 0.00, null, + -4.19, 990.39); + checkInst(model, 120, 120, LocalDate.of(2019, 5, 1), 83, 117, 50.00, 50.00, null, 0.88261372, 44.13, 3831.95, 4.14, 0.00, null, + -4.14, 990.39); + checkInst(model, 121, 121, LocalDate.of(2019, 5, 2), 82, 118, 50.00, 50.00, null, 0.88167226, 44.08, 3786.04, 4.09, 0.00, null, + -4.09, 990.39); + checkInst(model, 122, 122, LocalDate.of(2019, 5, 3), 81, 119, 50.00, 50.00, null, 0.88073180, 44.04, 3740.09, 4.04, 0.00, null, + -4.04, 990.39); + checkInst(model, 123, 123, LocalDate.of(2019, 5, 4), 80, 120, 50.00, 50.00, null, 0.87979234, 43.99, 3694.08, 3.99, 0.00, null, + -3.99, 990.39); + checkInst(model, 124, 124, LocalDate.of(2019, 5, 5), 79, 121, 50.00, 50.00, null, 0.87885389, 43.94, 3648.03, 3.94, 0.00, null, + -3.94, 990.39); + checkInst(model, 125, 125, LocalDate.of(2019, 5, 6), 78, 122, 50.00, 50.00, null, 0.87791644, 43.90, 3601.92, 3.90, 0.00, null, + -3.90, 990.39); + checkInst(model, 126, 126, LocalDate.of(2019, 5, 7), 77, 123, 50.00, 50.00, null, 0.87697999, 43.85, 3555.77, 3.85, 0.00, null, + -3.85, 990.39); + checkInst(model, 127, 127, LocalDate.of(2019, 5, 8), 76, 124, 50.00, 50.00, null, 0.87604453, 43.80, 3509.56, 3.80, 0.00, null, + -3.80, 990.39); + checkInst(model, 128, 128, LocalDate.of(2019, 5, 9), 75, 125, 50.00, 50.00, null, 0.87511008, 43.76, 3463.31, 3.75, 0.00, null, + -3.75, 990.39); + checkInst(model, 129, 129, LocalDate.of(2019, 5, 10), 74, 126, 50.00, 50.00, null, 0.87417662, 43.71, 3417.01, 3.70, 0.00, null, + -3.70, 990.39); + checkInst(model, 130, 130, LocalDate.of(2019, 5, 11), 73, 127, 50.00, 50.00, null, 0.87324416, 43.66, 3370.66, 3.65, 0.00, null, + -3.65, 990.39); + checkInst(model, 131, 131, LocalDate.of(2019, 5, 12), 72, 128, 50.00, 50.00, null, 0.87231269, 43.62, 3324.26, 3.60, 0.00, null, + -3.60, 990.39); + checkInst(model, 132, 132, LocalDate.of(2019, 5, 13), 71, 129, 50.00, 50.00, null, 0.87138221, 43.57, 3277.81, 3.55, 0.00, null, + -3.55, 990.39); + checkInst(model, 133, 133, LocalDate.of(2019, 5, 14), 70, 130, 50.00, 50.00, null, 0.87045273, 43.52, 3231.31, 3.50, 0.00, null, + -3.50, 990.39); + checkInst(model, 134, 134, LocalDate.of(2019, 5, 15), 69, 131, 50.00, 50.00, null, 0.86952424, 43.48, 3184.76, 3.45, 0.00, null, + -3.45, 990.39); + checkInst(model, 135, 135, LocalDate.of(2019, 5, 16), 68, 132, 50.00, 50.00, null, 0.86859674, 43.43, 3138.16, 3.40, 0.00, null, + -3.40, 990.39); + checkInst(model, 136, 136, LocalDate.of(2019, 5, 17), 67, 133, 50.00, 50.00, null, 0.86767023, 43.38, 3091.51, 3.35, 0.00, null, + -3.35, 990.39); + checkInst(model, 137, 137, LocalDate.of(2019, 5, 18), 66, 134, 50.00, 50.00, null, 0.86674471, 43.34, 3044.81, 3.30, 0.00, null, + -3.30, 990.39); + checkInst(model, 138, 138, LocalDate.of(2019, 5, 19), 65, 135, 50.00, 50.00, null, 0.86582017, 43.29, 2998.06, 3.25, 0.00, null, + -3.25, 990.39); + checkInst(model, 139, 139, LocalDate.of(2019, 5, 20), 64, 136, 50.00, 50.00, null, 0.86489662, 43.24, 2951.26, 3.20, 0.00, null, + -3.20, 990.39); + checkInst(model, 140, 140, LocalDate.of(2019, 5, 21), 63, 137, 50.00, 50.00, null, 0.86397406, 43.20, 2904.42, 3.15, 0.00, null, + -3.15, 990.39); + checkInst(model, 141, 141, LocalDate.of(2019, 5, 22), 62, 138, 50.00, 50.00, null, 0.86305248, 43.15, 2857.52, 3.10, 0.00, null, + -3.10, 990.39); + checkInst(model, 142, 142, LocalDate.of(2019, 5, 23), 61, 139, 50.00, 50.00, null, 0.86213188, 43.11, 2810.57, 3.05, 0.00, null, + -3.05, 990.39); + checkInst(model, 143, 143, LocalDate.of(2019, 5, 24), 60, 140, 50.00, 50.00, null, 0.86121227, 43.06, 2763.57, 3.00, 0.00, null, + -3.00, 990.39); + checkInst(model, 144, 144, LocalDate.of(2019, 5, 25), 59, 141, 50.00, 50.00, null, 0.86029363, 43.01, 2716.52, 2.95, 0.00, null, + -2.95, 990.39); + checkInst(model, 145, 145, LocalDate.of(2019, 5, 26), 58, 142, 50.00, 50.00, null, 0.85937598, 42.97, 2669.42, 2.90, 0.00, null, + -2.90, 990.39); + checkInst(model, 146, 146, LocalDate.of(2019, 5, 27), 57, 143, 50.00, 50.00, null, 0.85845930, 42.92, 2622.27, 2.85, 0.00, null, + -2.85, 990.39); + checkInst(model, 147, 147, LocalDate.of(2019, 5, 28), 56, 144, 50.00, 50.00, null, 0.85754361, 42.88, 2575.07, 2.80, 0.00, null, + -2.80, 990.39); + checkInst(model, 148, 148, LocalDate.of(2019, 5, 29), 55, 145, 50.00, 50.00, null, 0.85662889, 42.83, 2527.82, 2.75, 0.00, null, + -2.75, 990.39); + checkInst(model, 149, 149, LocalDate.of(2019, 5, 30), 54, 146, 50.00, 50.00, null, 0.85571514, 42.79, 2480.52, 2.70, 0.00, null, + -2.70, 990.39); + checkInst(model, 150, 150, LocalDate.of(2019, 5, 31), 53, 147, 50.00, 50.00, null, 0.85480237, 42.74, 2433.17, 2.65, 0.00, null, + -2.65, 990.39); + checkInst(model, 151, 151, LocalDate.of(2019, 6, 1), 52, 148, 50.00, 50.00, null, 0.85389057, 42.69, 2385.77, 2.60, 0.00, null, + -2.60, 990.39); + checkInst(model, 152, 152, LocalDate.of(2019, 6, 2), 51, 149, 50.00, 50.00, null, 0.85297975, 42.65, 2338.31, 2.55, 0.00, null, + -2.55, 990.39); + checkInst(model, 153, 153, LocalDate.of(2019, 6, 3), 50, 150, 50.00, 50.00, null, 0.85206990, 42.60, 2290.81, 2.50, 0.00, null, + -2.50, 990.39); + checkInst(model, 154, 154, LocalDate.of(2019, 6, 4), 49, 151, 50.00, 50.00, null, 0.85116101, 42.56, 2243.26, 2.45, 0.00, null, + -2.45, 990.39); + checkInst(model, 155, 155, LocalDate.of(2019, 6, 5), 48, 152, 50.00, 50.00, null, 0.85025310, 42.51, 2195.65, 2.40, 0.00, null, + -2.40, 990.39); + checkInst(model, 156, 156, LocalDate.of(2019, 6, 6), 47, 153, 50.00, 50.00, null, 0.84934616, 42.47, 2148.00, 2.34, 0.00, null, + -2.34, 990.39); + checkInst(model, 157, 157, LocalDate.of(2019, 6, 7), 46, 154, 50.00, 50.00, null, 0.84844018, 42.42, 2100.29, 2.29, 0.00, null, + -2.29, 990.39); + checkInst(model, 158, 158, LocalDate.of(2019, 6, 8), 45, 155, 50.00, 50.00, null, 0.84753517, 42.38, 2052.53, 2.24, 0.00, null, + -2.24, 990.39); + checkInst(model, 159, 159, LocalDate.of(2019, 6, 9), 44, 156, 50.00, 50.00, null, 0.84663113, 42.33, 2004.73, 2.19, 0.00, null, + -2.19, 990.39); + checkInst(model, 160, 160, LocalDate.of(2019, 6, 10), 43, 157, 50.00, 50.00, null, 0.84572805, 42.29, 1956.87, 2.14, 0.00, null, + -2.14, 990.39); + checkInst(model, 161, 161, LocalDate.of(2019, 6, 11), 42, 158, 50.00, 50.00, null, 0.84482593, 42.24, 1908.96, 2.09, 0.00, null, + -2.09, 990.39); + checkInst(model, 162, 162, LocalDate.of(2019, 6, 12), 41, 159, 50.00, 50.00, null, 0.84392477, 42.20, 1860.99, 2.04, 0.00, null, + -2.04, 990.39); + checkInst(model, 163, 163, LocalDate.of(2019, 6, 13), 40, 160, 50.00, 50.00, null, 0.84302458, 42.15, 1812.98, 1.99, 0.00, null, + -1.99, 990.39); + checkInst(model, 164, 164, LocalDate.of(2019, 6, 14), 39, 161, 50.00, 50.00, null, 0.84212535, 42.11, 1764.92, 1.94, 0.00, null, + -1.94, 990.39); + checkInst(model, 165, 165, LocalDate.of(2019, 6, 15), 38, 162, 50.00, 50.00, null, 0.84122707, 42.06, 1716.80, 1.88, 0.00, null, + -1.88, 990.39); + checkInst(model, 166, 166, LocalDate.of(2019, 6, 16), 37, 163, 50.00, 50.00, null, 0.84032975, 42.02, 1668.64, 1.83, 0.00, null, + -1.83, 990.39); + checkInst(model, 167, 167, LocalDate.of(2019, 6, 17), 36, 164, 50.00, 50.00, null, 0.83943340, 41.97, 1620.42, 1.78, 0.00, null, + -1.78, 990.39); + checkInst(model, 168, 168, LocalDate.of(2019, 6, 18), 35, 165, 50.00, 50.00, null, 0.83853799, 41.93, 1572.15, 1.73, 0.00, null, + -1.73, 990.39); + checkInst(model, 169, 169, LocalDate.of(2019, 6, 19), 34, 166, 50.00, 50.00, null, 0.83764354, 41.88, 1523.83, 1.68, 0.00, null, + -1.68, 990.39); + checkInst(model, 170, 170, LocalDate.of(2019, 6, 20), 33, 167, 50.00, 50.00, null, 0.83675005, 41.84, 1475.45, 1.63, 0.00, null, + -1.63, 990.39); + checkInst(model, 171, 171, LocalDate.of(2019, 6, 21), 32, 168, 50.00, 50.00, null, 0.83585751, 41.79, 1427.03, 1.58, 0.00, null, + -1.58, 990.39); + checkInst(model, 172, 172, LocalDate.of(2019, 6, 22), 31, 169, 50.00, 50.00, null, 0.83496592, 41.75, 1378.55, 1.52, 0.00, null, + -1.52, 990.39); + checkInst(model, 173, 173, LocalDate.of(2019, 6, 23), 30, 170, 50.00, 50.00, null, 0.83407528, 41.70, 1330.02, 1.47, 0.00, null, + -1.47, 990.39); + checkInst(model, 174, 174, LocalDate.of(2019, 6, 24), 29, 171, 50.00, 50.00, null, 0.83318560, 41.66, 1281.45, 1.42, 0.00, null, + -1.42, 990.39); + checkInst(model, 175, 175, LocalDate.of(2019, 6, 25), 28, 172, 50.00, 50.00, null, 0.83229686, 41.61, 1232.81, 1.37, 0.00, null, + -1.37, 990.39); + checkInst(model, 176, 176, LocalDate.of(2019, 6, 26), 27, 173, 50.00, 50.00, null, 0.83140907, 41.57, 1184.13, 1.32, 0.00, null, + -1.32, 990.39); + checkInst(model, 177, 177, LocalDate.of(2019, 6, 27), 26, 174, 50.00, 50.00, null, 0.83052222, 41.53, 1135.39, 1.26, 0.00, null, + -1.26, 990.39); + checkInst(model, 178, 178, LocalDate.of(2019, 6, 28), 25, 175, 50.00, 50.00, null, 0.82963633, 41.48, 1086.61, 1.21, 0.00, null, + -1.21, 990.39); + checkInst(model, 179, 179, LocalDate.of(2019, 6, 29), 24, 176, 50.00, 50.00, null, 0.82875137, 41.44, 1037.77, 1.16, 0.00, null, + -1.16, 990.39); + checkInst(model, 180, 180, LocalDate.of(2019, 6, 30), 23, 177, 50.00, 50.00, null, 0.82786736, 41.39, 988.88, 1.11, 0.00, null, + -1.11, 990.39); + checkInst(model, 181, 181, LocalDate.of(2019, 7, 1), 22, 178, 50.00, 50.00, null, 0.82698430, 41.35, 939.93, 1.06, 0.00, null, + -1.06, 990.39); + checkInst(model, 182, 182, LocalDate.of(2019, 7, 2), 21, 179, 50.00, 50.00, null, 0.82610217, 41.31, 890.93, 1.00, 0.00, null, + -1.00, 990.39); + checkInst(model, 183, 183, LocalDate.of(2019, 7, 3), 20, 180, 50.00, 50.00, null, 0.82522099, 41.26, 841.89, 0.95, 0.00, null, + -0.95, 990.39); + checkInst(model, 184, 184, LocalDate.of(2019, 7, 4), 19, 181, 50.00, 50.00, null, 0.82434075, 41.22, 792.79, 0.90, 0.00, null, + -0.90, 990.39); + checkInst(model, 185, 185, LocalDate.of(2019, 7, 5), 18, 182, 50.00, 50.00, null, 0.82346144, 41.17, 743.63, 0.85, 0.00, null, + -0.85, 990.39); + checkInst(model, 186, 186, LocalDate.of(2019, 7, 6), 17, 183, 50.00, 50.00, null, 0.82258308, 41.13, 694.43, 0.79, 0.00, null, + -0.79, 990.39); + checkInst(model, 187, 187, LocalDate.of(2019, 7, 7), 16, 184, 50.00, 50.00, null, 0.82170565, 41.09, 645.17, 0.74, 0.00, null, + -0.74, 990.39); + checkInst(model, 188, 188, LocalDate.of(2019, 7, 8), 15, 185, 50.00, 50.00, null, 0.82082916, 41.04, 595.86, 0.69, 0.00, null, + -0.69, 990.39); + checkInst(model, 189, 189, LocalDate.of(2019, 7, 9), 14, 186, 50.00, 50.00, null, 0.81995360, 41.00, 546.49, 0.64, 0.00, null, + -0.64, 990.39); + checkInst(model, 190, 190, LocalDate.of(2019, 7, 10), 13, 187, 50.00, 50.00, null, 0.81907897, 40.95, 497.08, 0.58, 0.00, null, + -0.58, 990.39); + checkInst(model, 191, 191, LocalDate.of(2019, 7, 11), 12, 188, 50.00, 50.00, null, 0.81820528, 40.91, 447.61, 0.53, 0.00, null, + -0.53, 990.39); + checkInst(model, 192, 192, LocalDate.of(2019, 7, 12), 11, 189, 50.00, 50.00, null, 0.81733252, 40.87, 398.08, 0.48, 0.00, null, + -0.48, 990.39); + checkInst(model, 193, 193, LocalDate.of(2019, 7, 13), 10, 190, 50.00, 50.00, null, 0.81646069, 40.82, 348.51, 0.43, 0.00, null, + -0.43, 990.39); + checkInst(model, 194, 194, LocalDate.of(2019, 7, 14), 9, 191, 50.00, 50.00, null, 0.81558979, 40.78, 298.88, 0.37, 0.00, null, + -0.37, 990.39); + checkInst(model, 195, 195, LocalDate.of(2019, 7, 15), 8, 192, 50.00, 50.00, null, 0.81471983, 40.74, 249.20, 0.32, 0.00, null, + -0.32, 990.39); + checkInst(model, 196, 196, LocalDate.of(2019, 7, 16), 7, 193, 50.00, 50.00, null, 0.81385078, 40.69, 199.47, 0.27, 0.00, null, + -0.27, 990.39); + checkInst(model, 197, 197, LocalDate.of(2019, 7, 17), 6, 194, 50.00, 50.00, null, 0.81298267, 40.65, 149.68, 0.21, 0.00, null, + -0.21, 990.39); + checkInst(model, 198, 198, LocalDate.of(2019, 7, 18), 5, 195, 50.00, 50.00, null, 0.81211548, 40.61, 99.84, 0.16, 0.00, null, -0.16, + 990.39); + checkInst(model, 199, 199, LocalDate.of(2019, 7, 19), 4, 196, 50.00, 50.00, null, 0.81124922, 40.56, 49.95, 0.11, 0.00, null, -0.11, + 990.39); + checkInst(model, 200, 200, LocalDate.of(2019, 7, 20), 3, 197, 50.00, 50.00, null, 0.81038388, 40.52, 0.00, 0.05, 0.00, null, -0.05, + 990.39); + + assertEquals(203, model.payments().size(), "disbursement + 200 regular + 2 additional"); + checkInst(model, 201, 201, LocalDate.of(2019, 7, 21), 2, 198, null, 50.00, null, 0.80951946, 40.48, null, null, 0.00, null, null, + null); + checkInst(model, 202, 202, LocalDate.of(2019, 7, 22), 1, 199, null, 50.00, null, 0.80865597, 40.43, null, null, 0.00, null, null, + null); + } + + @Test + void testLessPayment_term10_originationFee50_netDisbursement450_pay40() { + final BigDecimal smallOriginationFee = new BigDecimal("50"); + final BigDecimal smallNetDisbursement = new BigDecimal("450"); + final ProjectedAmortizationScheduleModel initial = calculator.generateModel(smallOriginationFee, smallNetDisbursement, TPV, RATE, + DAY_COUNT, EXPECTED_DISBURSEMENT_DATE, MC, CURRENCY); + final ProjectedAmortizationScheduleModel model = calculator.addDisbursement(initial, smallOriginationFee, smallNetDisbursement, + EXPECTED_DISBURSEMENT_DATE); + + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(1), new BigDecimal("40")); + + assertEquals(12, model.payments().size(), "disbursement + 10 regular + 1 tail"); + + checkInst(model, 0, 0, EXPECTED_DISBURSEMENT_DATE, 11, 0, -450.00, null, null, 1.00000000, -450.00, 450.00, null, null, null, null, + 50.00); + + checkInst(model, 1, 1, LocalDate.of(2019, 1, 2), 10, 0, 50.00, 50.00, 40.00, 1.00000000, 40.00, 408.83, 8.83, 7.07, 7.07, -1.77, + 42.93); + checkInst(model, 2, 2, LocalDate.of(2019, 1, 3), 9, 1, 50.00, 50.00, null, 0.98074794, 49.04, 366.86, 8.03, 0.00, null, -8.03, + 42.93); + checkInst(model, 3, 3, LocalDate.of(2019, 1, 4), 8, 2, 50.00, 50.00, null, 0.96186652, 48.09, 324.06, 7.20, 0.00, null, -7.20, + 42.93); + checkInst(model, 4, 4, LocalDate.of(2019, 1, 5), 7, 3, 50.00, 50.00, null, 0.94334860, 47.17, 280.42, 6.36, 0.00, null, -6.36, + 42.93); + checkInst(model, 5, 5, LocalDate.of(2019, 1, 6), 6, 4, 50.00, 50.00, null, 0.92518720, 46.26, 235.93, 5.50, 0.00, null, -5.50, + 42.93); + checkInst(model, 6, 6, LocalDate.of(2019, 1, 7), 5, 5, 50.00, 50.00, null, 0.90737544, 45.37, 190.56, 4.63, 0.00, null, -4.63, + 42.93); + checkInst(model, 7, 7, LocalDate.of(2019, 1, 8), 4, 6, 50.00, 50.00, null, 0.88990659, 44.50, 144.30, 3.74, 0.00, null, -3.74, + 42.93); + checkInst(model, 8, 8, LocalDate.of(2019, 1, 9), 3, 7, 50.00, 50.00, null, 0.87277405, 43.64, 97.13, 2.83, 0.00, null, -2.83, + 42.93); + checkInst(model, 9, 9, LocalDate.of(2019, 1, 10), 2, 8, 50.00, 50.00, null, 0.85597135, 42.80, 49.04, 1.91, 0.00, null, -1.91, + 42.93); + checkInst(model, 10, 10, LocalDate.of(2019, 1, 11), 1, 9, 50.00, 50.00, null, 0.83949214, 41.97, 0.00, 0.96, 0.00, null, -0.96, + 42.93); + + checkInst(model, 11, 11, LocalDate.of(2019, 1, 12), 0, 10, null, 10.00, null, 0.82333018, 8.23, null, null, 0.00, null, null, null); + } + + @Test + void testExcessPayment_term10_originationFee50_netDisbursement450_pay110() { + final BigDecimal smallOriginationFee = new BigDecimal("50"); + final BigDecimal smallNetDisbursement = new BigDecimal("450"); + final ProjectedAmortizationScheduleModel initial = calculator.generateModel(smallOriginationFee, smallNetDisbursement, TPV, RATE, + DAY_COUNT, EXPECTED_DISBURSEMENT_DATE, MC, CURRENCY); + final ProjectedAmortizationScheduleModel model = calculator.addDisbursement(initial, smallOriginationFee, smallNetDisbursement, + EXPECTED_DISBURSEMENT_DATE); + + calculator.applyPayment(model, EXPECTED_DISBURSEMENT_DATE.plusDays(1), new BigDecimal("110")); + + assertEquals(10, model.payments().size(), "disbursement + 9 regular (period 10 removed, forecast was 0)"); + + checkInst(model, 0, 0, EXPECTED_DISBURSEMENT_DATE, 11, 0, -450.00, null, null, 1.00000000, -450.00, 450.00, null, null, null, null, + 50.00); + + checkInst(model, 1, 1, LocalDate.of(2019, 1, 2), 10, 0, 50.00, 50.00, 110.00, 1.00000000, 110.00, 408.83, 8.83, 18.30, 18.30, 9.47, + 31.70); + checkInst(model, 2, 2, LocalDate.of(2019, 1, 3), 9, 1, 50.00, 50.00, null, 0.98074794, 49.04, 366.86, 8.03, 0.00, null, -8.03, + 31.70); + checkInst(model, 3, 3, LocalDate.of(2019, 1, 4), 8, 2, 50.00, 50.00, null, 0.96186652, 48.09, 324.06, 7.20, 0.00, null, -7.20, + 31.70); + checkInst(model, 4, 4, LocalDate.of(2019, 1, 5), 7, 3, 50.00, 50.00, null, 0.94334860, 47.17, 280.42, 6.36, 0.00, null, -6.36, + 31.70); + checkInst(model, 5, 5, LocalDate.of(2019, 1, 6), 6, 4, 50.00, 50.00, null, 0.92518720, 46.26, 235.93, 5.50, 0.00, null, -5.50, + 31.70); + checkInst(model, 6, 6, LocalDate.of(2019, 1, 7), 5, 5, 50.00, 50.00, null, 0.90737544, 45.37, 190.56, 4.63, 0.00, null, -4.63, + 31.70); + checkInst(model, 7, 7, LocalDate.of(2019, 1, 8), 4, 6, 50.00, 50.00, null, 0.88990659, 44.50, 144.30, 3.74, 0.00, null, -3.74, + 31.70); + checkInst(model, 8, 8, LocalDate.of(2019, 1, 9), 3, 7, 50.00, 50.00, null, 0.87277405, 43.64, 97.13, 2.83, 0.00, null, -2.83, + 31.70); + checkInst(model, 9, 9, LocalDate.of(2019, 1, 10), 2, 8, 50.00, 40.00, null, 0.85597135, 34.24, 49.04, 1.91, 0.00, null, -1.91, + 31.70); + } + + private ProjectedAmortizationScheduleModel generateModel() { + final ProjectedAmortizationScheduleModel model = calculator.generateModel(ORIGINATION_FEE, NET_DISBURSEMENT, TPV, RATE, DAY_COUNT, + EXPECTED_DISBURSEMENT_DATE, MC, CURRENCY); + return calculator.addDisbursement(model, ORIGINATION_FEE, NET_DISBURSEMENT, EXPECTED_DISBURSEMENT_DATE); + } + + private void checkInst(final ProjectedAmortizationScheduleModel model, final int index, final int expectedNo, + final LocalDate expectedDate, final long expectedCount, final long expectedPaymentsLeft, final Double expectedPayment, + final Double expectedForecastPayment, final Double expectedActualPayment, final Double expectedDiscountFactor, + final Double expectedNpvValue, final Double expectedBalance, final Double expectedAmortization, + final Double expectedNetAmortization, final Double expectedActualAmortization, final Double expectedIncomeModification, + final Double expectedDeferredBalance) { + final ProjectedPayment inst = model.payments().get(index); + final String p = "inst " + expectedNo + ": "; + + assertEquals(expectedNo, inst.paymentNo(), p + "paymentNo"); + assertEquals(expectedDate, inst.date(), p + "date"); + assertEquals(expectedCount, inst.count(), p + "count"); + assertEquals(expectedPaymentsLeft, inst.paymentsLeft(), p + "paymentsLeft"); + assertMoneyValue(expectedPayment, inst.expectedPaymentAmount(), 2, p + "expectedPayment"); + assertMoneyValue(expectedForecastPayment, inst.forecastPaymentAmount(), 2, p + "forecastPayment"); + assertMoneyValue(expectedActualPayment, inst.actualPaymentAmount(), 2, p + "actualPayment"); + assertValue(expectedDiscountFactor, inst.discountFactor(), 8, p + "discountFactor"); + assertMoneyValue(expectedNpvValue, inst.npvValue(), 2, p + "npvValue"); + assertMoneyValue(expectedBalance, inst.balance(), 2, p + "balance"); + assertMoneyValue(expectedAmortization, inst.expectedAmortizationAmount(), 2, p + "expectedAmortization"); + assertMoneyValue(expectedNetAmortization, inst.netAmortizationAmount(), 2, p + "netAmortization"); + assertMoneyValue(expectedActualAmortization, inst.actualAmortizationAmount(), 2, p + "actualAmort"); + assertMoneyValue(expectedIncomeModification, inst.incomeModification(), 2, p + "incomeModification"); + assertMoneyValue(expectedDeferredBalance, inst.deferredBalance(), 2, p + "deferredBalance"); + } + + private void assertMoneyValue(final Double expected, final Money actual, final int scale, final String msg) { + if (expected == null) { + assertNull(actual, msg); + return; + } + final BigDecimal exp = BigDecimal.valueOf(expected).setScale(scale, RoundingMode.HALF_UP); + final BigDecimal act = actual.getAmount().setScale(scale, RoundingMode.HALF_UP); + assertEquals(0, exp.compareTo(act), msg + " — expected: " + exp + ", actual: " + act); + } + + private void assertValue(final Double expected, final BigDecimal actual, final int scale, final String msg) { + if (expected == null) { + assertNull(actual, msg); + return; + } + final BigDecimal exp = BigDecimal.valueOf(expected).setScale(scale, RoundingMode.HALF_UP); + final BigDecimal act = actual.setScale(scale, RoundingMode.HALF_UP); + assertEquals(0, exp.compareTo(act), msg + " — expected: " + exp + ", actual: " + act); + } + +}