diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalBreachDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalBreachDataV1.avsc new file mode 100644 index 00000000000..10f967f787d --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalBreachDataV1.avsc @@ -0,0 +1,91 @@ +{ + "name": "WorkingCapitalBreachDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "breachFrequency", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "breachFrequencyType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "breachAmountCalculationType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "breachAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "breachGraceDays", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "breachStartDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "nearBreach", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalNearBreachDataV1" + ] + }, + { + "default": null, + "name": "breachSchedule", + "doc": "Current breach schedule periods.", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanBreachSchedulePeriodDataV1" + } + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanAccountDataV1.avsc new file mode 100644 index 00000000000..916e692c3e0 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanAccountDataV1.avsc @@ -0,0 +1,411 @@ +{ + "name": "WorkingCapitalLoanAccountDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "accountNo", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "externalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "status", + "type": [ + "null", + "org.apache.fineract.avro.loan.v1.LoanStatusEnumDataV1" + ] + }, + { + "default": null, + "name": "clientId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "clientAccountNo", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "clientName", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "clientOfficeId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "clientExternalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "loanProductId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "loanProductName", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "loanProductDescription", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "fundId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "fundName", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "principal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "approvedPrincipal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "proposedPrincipal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "netDisbursalAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "numberOfRepayments", + "doc": "Loan term in days, not a count of repayments. Null until the amortization schedule is generated.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "repaymentEvery", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "repaymentFrequencyType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "amortizationType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "graceOnArrearsAgeing", + "doc": "Grace days before a period counts as delinquent.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "timeline", + "type": [ + "null", + "org.apache.fineract.avro.loan.v1.LoanApplicationTimelineDataV1" + ] + }, + { + "default": null, + "name": "summary", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1" + ] + }, + { + "default": null, + "name": "charges", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1" + } + ] + }, + { + "default": null, + "name": "disbursementDetails", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDisbursementDetailDataV1" + } + ] + }, + { + "default": null, + "name": "totalOverpaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "loanProductCounter", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "delinquent", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1" + ] + }, + { + "default": null, + "name": "lastClosedBusinessDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargedOff", + "doc": "Always null: working capital does not yet implement charge-off.", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "enableInstallmentLevelDelinquency", + "doc": "True when the loan has a delinquency bucket configured. Working capital classifies delinquency per period, not per installment.", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "originators", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.OriginatorDetailsV1" + } + ] + }, + { + "default": null, + "name": "breach", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalBreachDataV1" + ] + }, + { + "default": null, + "name": "paymentRate", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "discountFee", + "doc": "Effective discount fee, set at disbursement.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "proposedDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "approvedDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "periodPaymentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "dailyEir", + "doc": "Daily effective interest rate derived from discount-fee pricing, not a lending interest rate.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "calculatedAnnualEir", + "doc": "Annualised dailyEir.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "npvDayCount", + "doc": "Day count used by the amortization schedule.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "totalPaymentVolume", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "delinquency", + "type": [ + "null", + "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyDataV1" + ] + }, + { + "default": null, + "name": "overpaidOnDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "actualNoTerm", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "customData", + "type": [ + "null", + { + "values": "bytes", + "type": "map" + } + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanBreachSchedulePeriodDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanBreachSchedulePeriodDataV1.avsc new file mode 100644 index 00000000000..14ea5f12293 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanBreachSchedulePeriodDataV1.avsc @@ -0,0 +1,79 @@ +{ + "name": "WorkingCapitalLoanBreachSchedulePeriodDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "periodNumber", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "fromDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "toDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "numberOfDays", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "minPaymentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "outstandingAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "nearBreach", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "breach", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "reset", + "type": [ + "null", + "boolean" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanChargeDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanChargeDataV1.avsc new file mode 100644 index 00000000000..15439afe6fc --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanChargeDataV1.avsc @@ -0,0 +1,179 @@ +{ + "name": "WorkingCapitalLoanChargeDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "chargeId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargeTimeType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" + ] + }, + { + "default": null, + "name": "dueDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargeCalculationType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" + ] + }, + { + "default": null, + "name": "currency", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.CurrencyDataV1" + ] + }, + { + "default": null, + "name": "amount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountAccrued", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "amountUnrecognized", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penalty", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "chargePaymentMode", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" + ] + }, + { + "default": null, + "name": "paid", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "loanId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "externalId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "externalLoanId", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "submittedOnDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "customData", + "type": [ + "null", + { + "values": "bytes", + "type": "map" + } + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanCollectionDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanCollectionDataV1.avsc new file mode 100644 index 00000000000..9fd134a7f12 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanCollectionDataV1.avsc @@ -0,0 +1,98 @@ +{ + "name": "WorkingCapitalLoanCollectionDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "delinquentDays", + "doc": "Days since the oldest active delinquency tag.", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "delinquentDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "delinquentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "lastPaymentDate", + "doc": "Date of the last non-reversed repayment-like transaction (repayment, payout refund, goodwill credit, charge adjustment).", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "lastPaymentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "lastRepaymentDate", + "doc": "Date of the last non-reversed repayment transaction.", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "lastRepaymentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "delinquencyPausePeriods", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.loan.v1.DelinquencyPausePeriodV1" + } + ] + }, + { + "default": null, + "name": "delinquencySchedule", + "doc": "Current delinquency schedule periods.", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1" + } + ] + }, + { + "default": null, + "name": "totalDelinquentAmount", + "doc": "Aggregated outstanding of the delinquency schedule periods; always equal to delinquentAmount.", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyDataV1.avsc new file mode 100644 index 00000000000..2bcb20622d0 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyDataV1.avsc @@ -0,0 +1,24 @@ +{ + "name": "WorkingCapitalLoanDelinquencyDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "delinquencyStartType", + "doc": "What starts the delinquency clock.", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "delinquencyStartDate", + "type": [ + "null", + "string" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencySchedulePeriodDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencySchedulePeriodDataV1.avsc new file mode 100644 index 00000000000..b53436485ab --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencySchedulePeriodDataV1.avsc @@ -0,0 +1,91 @@ +{ + "name": "WorkingCapitalLoanDelinquencySchedulePeriodDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "periodNumber", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "fromDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "toDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "expectedAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "paidAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "outstandingAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "minPaymentCriteriaMet", + "type": [ + "null", + "boolean" + ] + }, + { + "default": null, + "name": "delinquentDays", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "delinquentAmount", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "tags", + "doc": "Active delinquency range tags currently applied to this schedule period.", + "type": [ + "null", + { + "type": "array", + "items": "org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyScheduleTagDataV1" + } + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyScheduleTagDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyScheduleTagDataV1.avsc new file mode 100644 index 00000000000..a43c8f895a7 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDelinquencyScheduleTagDataV1.avsc @@ -0,0 +1,48 @@ +{ + "name": "WorkingCapitalLoanDelinquencyScheduleTagDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "rangeId", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "classification", + "doc": "Name of the delinquency range this schedule period is currently classified into.", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "minimumAgeDays", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "maximumAgeDays", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "delinquentAmount", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDisbursementDetailDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDisbursementDetailDataV1.avsc new file mode 100644 index 00000000000..cec630873f8 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanDisbursementDetailDataV1.avsc @@ -0,0 +1,48 @@ +{ + "name": "WorkingCapitalLoanDisbursementDetailDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "expectedDisbursementDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "actualDisbursementDate", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "principal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "netDisbursalAmount", + "doc": "Net amount disbursed to the client after up-front deductions (from the amortization model).", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanSummaryDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanSummaryDataV1.avsc new file mode 100644 index 00000000000..e01c157c357 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalLoanSummaryDataV1.avsc @@ -0,0 +1,321 @@ +{ + "name": "WorkingCapitalLoanSummaryDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "currency", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.CurrencyDataV1" + ] + }, + { + "default": null, + "name": "principalDisbursed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalAdjustments", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "principalOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesCharged", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "feeChargesOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesCharged", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesPaid", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "penaltyChargesOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalExpectedRepayment", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRepayment", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalOutstanding", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalWrittenOff", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRecovered", + "doc": "Always null: working capital does not yet implement recovery.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "writeoffReasonId", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "writeoffReason", + "doc": "Always null: working capital does not yet implement write-off.", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "chargeOffReasonId", + "doc": "Always null: working capital does not yet implement charge-off.", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "chargeOffReason", + "doc": "Always null: working capital does not yet implement charge-off.", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "totalPayoutRefund", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalPayoutRefundReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalGoodwillCredit", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalGoodwillCreditReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalChargeAdjustment", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalChargeAdjustmentReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalCreditBalanceRefund", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalCreditBalanceRefundReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRepaymentTransaction", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalRepaymentTransactionReversed", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalPrincipal", + "doc": "Disbursed amount plus the discount fee.", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "realizedIncomeFromDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "unrealizedIncomeFromDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalDiscountFee", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalDiscountFeeAdjustment", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalNearBreachDataV1.avsc b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalNearBreachDataV1.avsc new file mode 100644 index 00000000000..01aaf251677 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/workingcapitalloan/v1/WorkingCapitalNearBreachDataV1.avsc @@ -0,0 +1,47 @@ +{ + "name": "WorkingCapitalNearBreachDataV1", + "namespace": "org.apache.fineract.avro.workingcapitalloan.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "long" + ] + }, + { + "default": null, + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "frequency", + "type": [ + "null", + "int" + ] + }, + { + "default": null, + "name": "frequencyType", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, + { + "default": null, + "name": "threshold", + "type": [ + "null", + "bigdecimal" + ] + } + ] +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java index d9bcc74c6f0..7f9b07674a4 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/ErrorMessageHelper.java @@ -1151,7 +1151,7 @@ public static String discountAdjustmentUndoTransactionNotFoundFailure() { } public static String discountAdjustmentUndoNotActiveLoanFailure() { - return "Failed data validation due to: undo.discount.adjustment.only.allowed.for.active.loan."; + return "Failed data validation due to: undo.transaction.not.allowed.for.loan.status."; } public static String nearBreachCannotEnableWithoutBreachFailure() { diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java index 90cf8dd9ffa..99aa90d340c 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/EventCheckHelper.java @@ -24,34 +24,57 @@ import java.math.BigDecimal; import java.math.MathContext; +import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.avro.client.v1.ClientDataV1; import org.apache.fineract.avro.loan.v1.DelinquencyPausePeriodV1; import org.apache.fineract.avro.loan.v1.LoanAccountDataV1; import org.apache.fineract.avro.loan.v1.LoanAmountDataV1; +import org.apache.fineract.avro.loan.v1.LoanApplicationTimelineDataV1; import org.apache.fineract.avro.loan.v1.LoanInstallmentDelinquencyBucketDataV1; import org.apache.fineract.avro.loan.v1.LoanOwnershipTransferDataV1; import org.apache.fineract.avro.loan.v1.LoanTransactionAdjustmentDataV1; import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalBreachDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyScheduleTagDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanTransactionDataV1; import org.apache.fineract.client.feign.FineractFeignClient; import org.apache.fineract.client.models.ExternalTransferData; +import org.apache.fineract.client.models.ExternalTransferDataDetails; import org.apache.fineract.client.models.GetClientsClientIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdDelinquencyPausePeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactions; +import org.apache.fineract.client.models.GetWorkingCapitalLoanSummary; import org.apache.fineract.client.models.GetWorkingCapitalLoanTransactionIdResponse; import org.apache.fineract.client.models.GetWorkingCapitalLoanTransactionsResponse; +import org.apache.fineract.client.models.GetWorkingCapitalLoansLoanIdResponse; +import org.apache.fineract.client.models.GetWorkingCapitalLoansLoanIdTimeline; import org.apache.fineract.client.models.GlobalConfigurationPropertyData; import org.apache.fineract.client.models.PageExternalTransferData; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.models.WorkingCapitalCollection; +import org.apache.fineract.client.models.WorkingCapitalCollectionDelinquencyPausePeriod; +import org.apache.fineract.client.models.WorkingCapitalCollectionRangeScheduleDelinquency; import org.apache.fineract.test.data.AssetExternalizationTransferStatus; import org.apache.fineract.test.data.AssetExternalizationTransferStatusReason; import org.apache.fineract.test.data.TransactionType; @@ -83,6 +106,18 @@ import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionMerchantIssuedRefundPostEvent; import org.apache.fineract.test.messaging.event.loan.transaction.LoanTransactionPayoutRefundPostEvent; import org.apache.fineract.test.messaging.event.loan.transaction.LoanUndoContractTerminationBusinessEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.AbstractWorkingCapitalLoanEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanApplicationModifiedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanApprovedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanBalanceChangedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanCreatedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanDelinquencyRangeChangeEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanDisbursalEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanRejectedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanStatusChangedEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanUndoApprovalEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.loan.WorkingCapitalLoanUndoDisbursalEvent; +import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.AbstractWorkingCapitalLoanTransactionEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanDisbursalTransactionBusinessEvent; import org.apache.fineract.test.messaging.event.workingcapitalloan.transaction.WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent; @@ -110,9 +145,7 @@ public class EventCheckHelper { private org.apache.fineract.test.messaging.config.EventProperties eventProperties; public void waitForTransactionCommit() { - if (TRANSACTION_COMMIT_DELAY_MS > 0) { - sleepIfEventVerificationEnabled(TRANSACTION_COMMIT_DELAY_MS); - } + sleepIfEventVerificationEnabled(TRANSACTION_COMMIT_DELAY_MS); } public void sleepIfEventVerificationEnabled(long sleepInMs) { @@ -186,70 +219,43 @@ private void loanAccountDataV1Check(Class eventClaz eventAssertion.assertEvent(eventClazz, loanId)// .extractingData(loanAccountDataV1 -> { - Long idActual = loanAccountDataV1.getId(); - Long idExpected = body.getId(); - Long statusIdActual = loanAccountDataV1.getStatus().getId().longValue(); - Long statusIdExpected = body.getStatus().getId(); - String statusCodeActual = loanAccountDataV1.getStatus().getCode(); - String statusCodeExpected = body.getStatus().getCode(); - Long clientIdActual = loanAccountDataV1.getClientId(); - Long clientIdExpected = body.getClientId(); - BigDecimal principalDisbursedActual = loanAccountDataV1.getSummary().getPrincipalDisbursed(); - Double principalDisbursedExpectedDouble = body.getSummary().getPrincipalDisbursed().doubleValue(); - BigDecimal principalDisbursedExpected = BigDecimal.valueOf(principalDisbursedExpectedDouble); - String actualDisbursementDateActual = loanAccountDataV1.getTimeline().getActualDisbursementDate(); - String actualDisbursementDateExpected = FORMATTER_EVENTS.format(body.getTimeline().getActualDisbursementDate()); - String currencyCodeActual = loanAccountDataV1.getSummary().getCurrency().getCode(); - String currencyCodeExpected = body.getSummary().getCurrency().getCode(); - BigDecimal totalUnpaidPayableDueInterestActual = loanAccountDataV1.getSummary().getTotalUnpaidPayableDueInterest(); - BigDecimal totalUnpaidPayableDueInterestExpected = body.getSummary().getTotalUnpaidPayableDueInterest(); - BigDecimal totalUnpaidPayableNotDueInterestActual = loanAccountDataV1.getSummary() - .getTotalUnpaidPayableNotDueInterest(); - BigDecimal totalUnpaidPayableNotDueInterestExpected = body.getSummary().getTotalUnpaidPayableNotDueInterest(); - BigDecimal totalInterestPaymentWaiverActual = loanAccountDataV1.getSummary().getTotalInterestPaymentWaiver(); - Double totalInterestPaymentWaiverExpectedDouble = body.getSummary().getTotalInterestPaymentWaiver().doubleValue(); - BigDecimal totalInterestPaymentWaiverExpected = new BigDecimal(totalInterestPaymentWaiverExpectedDouble, - MathContext.DECIMAL64); - BigDecimal delinquentInterestActual = loanAccountDataV1.getDelinquent().getDelinquentInterest(); - BigDecimal delinquentInterestExpected = body.getDelinquent().getDelinquentInterest(); - BigDecimal delinquentFeeActual = loanAccountDataV1.getDelinquent().getDelinquentFee(); - BigDecimal delinquentFeeExpected = body.getDelinquent().getDelinquentFee(); - BigDecimal delinquentPenaltyActual = loanAccountDataV1.getDelinquent().getDelinquentPenalty(); - BigDecimal delinquentPenaltyExpected = body.getDelinquent().getDelinquentPenalty(); - Integer actualNoTermActual = loanAccountDataV1.getActualNoTerm(); - Integer actualNoTermExpected = body.getActualNoTerm(); - - assertThat(idActual).isEqualTo(idExpected); - assertThat(statusIdActual).isEqualTo(statusIdExpected); - assertThat(statusCodeActual).isEqualTo(statusCodeExpected); - assertThat(clientIdActual).isEqualTo(clientIdExpected); - assertThat(areBigDecimalValuesEqual(principalDisbursedActual, principalDisbursedExpected)).isTrue(); - assertThat(actualDisbursementDateActual).isEqualTo(actualDisbursementDateExpected); - assertThat(currencyCodeActual).isEqualTo(currencyCodeExpected); - assertThat(areBigDecimalValuesEqual(totalUnpaidPayableDueInterestActual, totalUnpaidPayableDueInterestExpected)) - .isTrue(); - assertThat(areBigDecimalValuesEqual(totalUnpaidPayableNotDueInterestActual, totalUnpaidPayableNotDueInterestExpected)) - .isTrue(); - assertThat(areBigDecimalValuesEqual(totalInterestPaymentWaiverActual, totalInterestPaymentWaiverExpected)).isTrue(); - assertThat(areBigDecimalValuesEqual(delinquentInterestActual, delinquentInterestExpected)).isTrue(); - assertThat(areBigDecimalValuesEqual(delinquentFeeActual, delinquentFeeExpected)).isTrue(); - assertThat(areBigDecimalValuesEqual(delinquentPenaltyActual, delinquentPenaltyExpected)).isTrue(); - assertThat(actualNoTermActual).isEqualTo(actualNoTermExpected); - + assertThat(loanAccountDataV1.getId()).as("id").isEqualTo(body.getId()); + assertThat(loanAccountDataV1.getStatus().getId().longValue()).as("status.id").isEqualTo(body.getStatus().getId()); + assertThat(loanAccountDataV1.getStatus().getCode()).as("status.code").isEqualTo(body.getStatus().getCode()); + assertThat(loanAccountDataV1.getClientId()).as("clientId").isEqualTo(body.getClientId()); + assertAmountEquals("summary.principalDisbursed", loanAccountDataV1.getSummary().getPrincipalDisbursed(), + BigDecimal.valueOf(body.getSummary().getPrincipalDisbursed().doubleValue())); + assertThat(loanAccountDataV1.getTimeline().getActualDisbursementDate()).as("timeline.actualDisbursementDate") + .isEqualTo(FORMATTER_EVENTS.format(body.getTimeline().getActualDisbursementDate())); + assertThat(loanAccountDataV1.getSummary().getCurrency().getCode()).as("summary.currency.code") + .isEqualTo(body.getSummary().getCurrency().getCode()); + assertAmountEquals("summary.totalUnpaidPayableDueInterest", + loanAccountDataV1.getSummary().getTotalUnpaidPayableDueInterest(), + body.getSummary().getTotalUnpaidPayableDueInterest()); + assertAmountEquals("summary.totalUnpaidPayableNotDueInterest", + loanAccountDataV1.getSummary().getTotalUnpaidPayableNotDueInterest(), + body.getSummary().getTotalUnpaidPayableNotDueInterest()); + assertAmountEquals("summary.totalInterestPaymentWaiver", loanAccountDataV1.getSummary().getTotalInterestPaymentWaiver(), + new BigDecimal(body.getSummary().getTotalInterestPaymentWaiver().doubleValue(), MathContext.DECIMAL64)); + assertAmountEquals("delinquent.delinquentInterest", loanAccountDataV1.getDelinquent().getDelinquentInterest(), + body.getDelinquent().getDelinquentInterest()); + assertAmountEquals("delinquent.delinquentFee", loanAccountDataV1.getDelinquent().getDelinquentFee(), + body.getDelinquent().getDelinquentFee()); + assertAmountEquals("delinquent.delinquentPenalty", loanAccountDataV1.getDelinquent().getDelinquentPenalty(), + body.getDelinquent().getDelinquentPenalty()); + assertThat(loanAccountDataV1.getActualNoTerm()).as("actualNoTerm").isEqualTo(body.getActualNoTerm()); return null; }); } public GetLoansLoanIdTransactions getNthTransactionType(String nthItemStr, String transactionType, String transactionDate, List transactions) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); int nthItem = Integer.parseInt(nthItemStr) - 1; - GetLoansLoanIdTransactions targetTransaction = transactions// + return transactions// .stream()// - .filter(t -> transactionDate.equals(formatter.format(t.getDate())) && transactionType.equals(t.getType().getValue()))// + .filter(t -> transactionDate.equals(DATE_FORMATTER.format(t.getDate())) && transactionType.equals(t.getType().getValue()))// .toList()// .get(nthItem);// - return targetTransaction; } public GetLoansLoanIdTransactions findNthTransaction(String nthItemStr, String transactionType, String transactionDate, long loanId) { @@ -278,11 +284,8 @@ public void loanUndoContractTerminationEventCheck(final GetLoansLoanIdTransactio eventAssertion.assertEventRaised(LoanUndoContractTerminationBusinessEvent.class, transaction.getId()); } - private boolean areBigDecimalValuesEqual(BigDecimal actual, BigDecimal expected) { - log.debug("--- Checking BigDecimal values.... ---"); - log.debug("Actual: {}", actual); - log.debug("Expected: {}", expected); - return actual.compareTo(expected) == 0; + private static void assertAmountEquals(final String description, final BigDecimal actual, final BigDecimal expected) { + assertThat(actual).as(description).isEqualByComparingTo(expected); } public void loanDisbursalTransactionEventCheck(PostLoansLoanIdResponse loanDisburseResponse) { @@ -310,43 +313,18 @@ public void workingCapitalLoanDisbursalTransactionEventCheck(final Long loanId) public void workingCapitalLoanDisbursalTransactionEventCheck(final Long loanId, final BigDecimal expectedAmount) { waitForTransactionCommit(); - final GetWorkingCapitalLoanTransactionsResponse body = ok( - () -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTransactionsById(loanId)); - if (body.getContent() == null || body.getContent().isEmpty()) { - throw new IllegalStateException("No Working Capital Loan transactions found"); - } - - final GetWorkingCapitalLoanTransactionIdResponse disbursementTransaction = body.getContent().stream() - .filter(t -> t.getType() != null && "loanTransactionType.disbursement".equals(t.getType().getCode()) - && !Boolean.TRUE.equals(t.getReversed())) - .reduce((first, second) -> second).orElseThrow(() -> new IllegalStateException("Disbursement transaction not found")); - - eventAssertion.assertEvent(WorkingCapitalLoanDisbursalTransactionBusinessEvent.class, disbursementTransaction.getId())// - .extractingData(WorkingCapitalLoanTransactionDataV1::getWcLoanId).isEqualTo(loanId)// - .extractingBigDecimal(WorkingCapitalLoanTransactionDataV1::getTransactionAmount) - .isEqualTo(expectedAmount == null ? disbursementTransaction.getTransactionAmount() : expectedAmount)// - .extractingData(WorkingCapitalLoanTransactionDataV1::getReversed).isEqualTo(Boolean.FALSE); + final GetWorkingCapitalLoanTransactionIdResponse disbursementTransaction = findLastWorkingCapitalLoanTransaction(loanId, + "disbursement", false, "Disbursement transaction not found"); + workingCapitalLoanTransactionEventCheck(WorkingCapitalLoanDisbursalTransactionBusinessEvent.class, loanId, disbursementTransaction, + expectedAmount, false); } public void workingCapitalLoanCreditBalanceRefundTransactionEventCheck(final Long loanId, final BigDecimal expectedAmount) { waitForTransactionCommit(); - final GetWorkingCapitalLoanTransactionsResponse body = ok( - () -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTransactionsById(loanId)); - if (body.getContent() == null || body.getContent().isEmpty()) { - throw new IllegalStateException("No Working Capital Loan transactions found"); - } - - final GetWorkingCapitalLoanTransactionIdResponse cbrTransaction = body.getContent().stream() - .filter(t -> t.getType() != null && "loanTransactionType.creditBalanceRefund".equals(t.getType().getCode()) - && !Boolean.TRUE.equals(t.getReversed())) - .reduce((first, second) -> second) - .orElseThrow(() -> new IllegalStateException("Credit balance refund transaction not found")); - - eventAssertion.assertEvent(WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent.class, cbrTransaction.getId())// - .extractingData(WorkingCapitalLoanTransactionDataV1::getWcLoanId).isEqualTo(loanId)// - .extractingBigDecimal(WorkingCapitalLoanTransactionDataV1::getTransactionAmount) - .isEqualTo(expectedAmount == null ? cbrTransaction.getTransactionAmount() : expectedAmount)// - .extractingData(WorkingCapitalLoanTransactionDataV1::getReversed).isEqualTo(Boolean.FALSE); + final GetWorkingCapitalLoanTransactionIdResponse cbrTransaction = findLastWorkingCapitalLoanTransaction(loanId, + "creditBalanceRefund", false, "Credit balance refund transaction not found"); + workingCapitalLoanTransactionEventCheck(WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent.class, loanId, cbrTransaction, + expectedAmount, false); } public void workingCapitalLoanUndoDisbursalTransactionEventCheck(final Long loanId) { @@ -355,47 +333,49 @@ public void workingCapitalLoanUndoDisbursalTransactionEventCheck(final Long loan public void workingCapitalLoanUndoDisbursalTransactionEventCheck(final Long loanId, final BigDecimal expectedAmount) { waitForTransactionCommit(); - final GetWorkingCapitalLoanTransactionsResponse body = ok( - () -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTransactionsById(loanId)); - if (body.getContent() == null || body.getContent().isEmpty()) { - throw new IllegalStateException("No Working Capital Loan transactions found"); - } - - final GetWorkingCapitalLoanTransactionIdResponse reversedDisbursementTransaction = body.getContent().stream() - .filter(t -> t.getType() != null && "loanTransactionType.disbursement".equals(t.getType().getCode()) - && Boolean.TRUE.equals(t.getReversed())) - .reduce((first, second) -> second) - .orElseThrow(() -> new IllegalStateException("Reversed disbursement transaction not found")); - - eventAssertion.assertEvent(WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent.class, reversedDisbursementTransaction.getId())// - .extractingData(WorkingCapitalLoanTransactionDataV1::getWcLoanId).isEqualTo(loanId)// - .extractingBigDecimal(WorkingCapitalLoanTransactionDataV1::getTransactionAmount) - .isEqualTo(expectedAmount == null ? reversedDisbursementTransaction.getTransactionAmount() : expectedAmount)// - .extractingData(WorkingCapitalLoanTransactionDataV1::getReversed).isEqualTo(Boolean.TRUE); + final GetWorkingCapitalLoanTransactionIdResponse reversedDisbursementTransaction = findLastWorkingCapitalLoanTransaction(loanId, + "disbursement", true, "Reversed disbursement transaction not found"); + workingCapitalLoanTransactionEventCheck(WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent.class, loanId, + reversedDisbursementTransaction, expectedAmount, true); } public void workingCapitalLoanDiscountFeeTransactionEventCheck(final Long loanId, String transactionType, final BigDecimal expectedAmount, String transactionDate) { final GetWorkingCapitalLoanTransactionIdResponse discountFeeTransaction = workingCapitalLoanTransactionDetails(loanId, transactionType, transactionDate); - - eventAssertion.assertEvent(WorkingCapitalLoanDiscountFeeTransactionBusinessEvent.class, discountFeeTransaction.getId())// - .extractingData(WorkingCapitalLoanTransactionDataV1::getWcLoanId).isEqualTo(loanId)// - .extractingBigDecimal(WorkingCapitalLoanTransactionDataV1::getTransactionAmount) - .isEqualTo(expectedAmount == null ? discountFeeTransaction.getTransactionAmount() : expectedAmount)// - .extractingData(WorkingCapitalLoanTransactionDataV1::getReversed).isEqualTo(Boolean.FALSE); + workingCapitalLoanTransactionEventCheck(WorkingCapitalLoanDiscountFeeTransactionBusinessEvent.class, loanId, discountFeeTransaction, + expectedAmount, false); } public void workingCapitalLoanDiscountFeeAdjustmentTransactionEventCheck(final Long loanId, String transactionType, final BigDecimal expectedAmount, String transactionDate) { final GetWorkingCapitalLoanTransactionIdResponse discountFeeTransaction = workingCapitalLoanTransactionDetails(loanId, transactionType, transactionDate); + workingCapitalLoanTransactionEventCheck(WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent.class, loanId, + discountFeeTransaction, expectedAmount, false); + } + + private GetWorkingCapitalLoanTransactionIdResponse findLastWorkingCapitalLoanTransaction(final Long loanId, + final String transactionType, final boolean reversed, final String notFoundMessage) { + final GetWorkingCapitalLoanTransactionsResponse body = ok( + () -> fineractClient.workingCapitalLoanTransactions().retrieveWorkingCapitalLoanTransactionsById(loanId)); + if (body.getContent() == null || body.getContent().isEmpty()) { + throw new IllegalStateException("No Working Capital Loan transactions found"); + } + final String expectedCode = "loanTransactionType." + transactionType; + return body.getContent().stream().filter( + t -> t.getType() != null && expectedCode.equals(t.getType().getCode()) && reversed == Boolean.TRUE.equals(t.getReversed())) + .reduce((first, second) -> second).orElseThrow(() -> new IllegalStateException(notFoundMessage)); + } - eventAssertion.assertEvent(WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent.class, discountFeeTransaction.getId())// + private void workingCapitalLoanTransactionEventCheck(final Class eventClazz, + final Long loanId, final GetWorkingCapitalLoanTransactionIdResponse transaction, final BigDecimal expectedAmount, + final boolean expectedReversed) { + eventAssertion.assertEvent(eventClazz, transaction.getId())// .extractingData(WorkingCapitalLoanTransactionDataV1::getWcLoanId).isEqualTo(loanId)// .extractingBigDecimal(WorkingCapitalLoanTransactionDataV1::getTransactionAmount) - .isEqualTo(expectedAmount == null ? discountFeeTransaction.getTransactionAmount() : expectedAmount)// - .extractingData(WorkingCapitalLoanTransactionDataV1::getReversed).isEqualTo(Boolean.FALSE); + .isEqualTo(expectedAmount == null ? transaction.getTransactionAmount() : expectedAmount)// + .extractingData(WorkingCapitalLoanTransactionDataV1::getReversed).isEqualTo(expectedReversed); } public GetWorkingCapitalLoanTransactionIdResponse workingCapitalLoanTransactionDetails(final Long loanId, String transactionType, @@ -454,129 +434,74 @@ public EventAssertion.EventAssertionBuilder transactionEv public void loanOwnershipTransferBusinessEventCheck(Long loanId, Long transferId) { waitForTransactionCommit(); - PageExternalTransferData response = ok(() -> fineractClient.externalAssetOwners().getTransfers(Map.of("loanId", loanId))); - List content = response.getContent(); - - ExternalTransferData filtered = content.stream().filter(t -> transferId.equals(t.getTransferId())).reduce((first, second) -> second) - .orElseThrow(() -> new IllegalStateException("No element found")); - - BigDecimal totalOutstandingBalanceAmountExpected = zeroConversion(filtered.getDetails().getTotalOutstanding()); - BigDecimal outstandingPrincipalPortionExpected = zeroConversion(filtered.getDetails().getTotalPrincipalOutstanding()); - BigDecimal outstandingFeePortionExpected = zeroConversion(filtered.getDetails().getTotalFeeChargesOutstanding()); - BigDecimal outstandingPenaltyPortionExpected = zeroConversion(filtered.getDetails().getTotalPenaltyChargesOutstanding()); - BigDecimal outstandingInterestPortionExpected = zeroConversion(filtered.getDetails().getTotalInterestOutstanding()); - BigDecimal overPaymentPortionExpected = zeroConversion(filtered.getDetails().getTotalOverpaid()); - - eventAssertion.assertEvent(LoanOwnershipTransferEvent.class, loanId).extractingData(LoanOwnershipTransferDataV1::getLoanId) - .isEqualTo(loanId).extractingData(LoanOwnershipTransferDataV1::getAssetOwnerExternalId) - .isEqualTo(filtered.getOwner().getExternalId()).extractingData(LoanOwnershipTransferDataV1::getTransferExternalId) - .isEqualTo(filtered.getTransferExternalId()).extractingData(LoanOwnershipTransferDataV1::getSettlementDate) - .isEqualTo(FORMATTER_EVENTS.format(filtered.getSettlementDate())) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getTotalOutstandingBalanceAmount) - .isEqualTo(totalOutstandingBalanceAmountExpected) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPrincipalPortion) - .isEqualTo(outstandingPrincipalPortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingFeePortion) - .isEqualTo(outstandingFeePortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPenaltyPortion) - .isEqualTo(outstandingPenaltyPortionExpected) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingInterestPortion) - .isEqualTo(outstandingInterestPortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOverPaymentPortion) - .isEqualTo(overPaymentPortionExpected); + ExternalTransferData filtered = findLastTransfer(loanId, transferId); + loanOwnershipTransferEventBaseCheck(loanId, filtered, expectedAmountsOf(filtered.getDetails())); } public void loanOwnershipTransferBusinessEventWithStatusCheck(Long loanId, Long transferId, String transferStatus, String transferStatusReason) { - PageExternalTransferData response = ok(() -> fineractClient.externalAssetOwners().getTransfers(Map.of("loanId", loanId))); - List content = response.getContent(); - - ExternalTransferData filtered = content.stream().filter(t -> transferId.equals(t.getTransferId())).reduce((first, second) -> second) - .orElseThrow(() -> new IllegalStateException("No element found")); + ExternalTransferData filtered = findLastTransfer(loanId, transferId); + ExpectedTransferAmounts expected = filtered.getDetails() == null ? ExpectedTransferAmounts.EMPTY + : expectedAmountsOf(filtered.getDetails()); - BigDecimal totalOutstandingBalanceAmountExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalOutstanding()); - BigDecimal outstandingPrincipalPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalPrincipalOutstanding()); - BigDecimal outstandingFeePortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalFeeChargesOutstanding()); - BigDecimal outstandingPenaltyPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalPenaltyChargesOutstanding()); - BigDecimal outstandingInterestPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalInterestOutstanding()); - BigDecimal overPaymentPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalOverpaid()); - - AssetExternalizationTransferStatus transferStatusType = AssetExternalizationTransferStatus.valueOf(transferStatus); - String transferStatusExpected = transferStatusType.getValue(); - - AssetExternalizationTransferStatusReason transferStatusReasonType = AssetExternalizationTransferStatusReason - .valueOf(transferStatusReason); - String transferStatusReasonExpected = transferStatusReasonType.getValue(); - - eventAssertion.assertEvent(LoanOwnershipTransferEvent.class, loanId).extractingData(LoanOwnershipTransferDataV1::getLoanId) - .isEqualTo(loanId).extractingData(LoanOwnershipTransferDataV1::getAssetOwnerExternalId) - .isEqualTo(filtered.getOwner().getExternalId()).extractingData(LoanOwnershipTransferDataV1::getTransferExternalId) - .isEqualTo(filtered.getTransferExternalId()).extractingData(LoanOwnershipTransferDataV1::getSettlementDate) - .isEqualTo(FORMATTER_EVENTS.format(filtered.getSettlementDate())) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getTotalOutstandingBalanceAmount) - .isEqualTo(totalOutstandingBalanceAmountExpected) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPrincipalPortion) - .isEqualTo(outstandingPrincipalPortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingFeePortion) - .isEqualTo(outstandingFeePortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPenaltyPortion) - .isEqualTo(outstandingPenaltyPortionExpected) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingInterestPortion) - .isEqualTo(outstandingInterestPortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOverPaymentPortion) - .isEqualTo(overPaymentPortionExpected).extractingData(LoanOwnershipTransferDataV1::getTransferStatus) - .isEqualTo(transferStatusExpected).extractingData(LoanOwnershipTransferDataV1::getTransferStatusReason) - .isEqualTo(transferStatusReasonExpected); + loanOwnershipTransferEventBaseCheck(loanId, filtered, expected)// + .extractingData(LoanOwnershipTransferDataV1::getTransferStatus) + .isEqualTo(AssetExternalizationTransferStatus.valueOf(transferStatus).getValue())// + .extractingData(LoanOwnershipTransferDataV1::getTransferStatusReason) + .isEqualTo(AssetExternalizationTransferStatusReason.valueOf(transferStatusReason).getValue()); } public void loanOwnershipTransferBusinessEventWithTypeCheck(Long loanId, ExternalTransferData transferData, String transferType, String previousAssetOwner) { - PageExternalTransferData response = ok(() -> fineractClient.externalAssetOwners().getTransfers(Map.of("loanId", loanId))); - List content = response.getContent(); - Long transferId = transferData.getTransferId(); String assetOwner = transferData.getOwner() == null ? null : transferData.getOwner().getExternalId(); + ExternalTransferData filtered = findLastTransfer(loanId, transferData.getTransferId()); + ExpectedTransferAmounts expected = filtered.getDetails() == null ? ExpectedTransferAmounts.EMPTY + : expectedAmountsOf(filtered.getDetails()); + + loanOwnershipTransferEventBaseCheck(loanId, filtered, expected)// + .extractingData(LoanOwnershipTransferDataV1::getType).isEqualTo(transferType)// + .extractingData(LoanOwnershipTransferDataV1::getAssetOwnerExternalId).isEqualTo(assetOwner)// + .extractingData(LoanOwnershipTransferDataV1::getPreviousOwnerExternalId).isEqualTo(previousAssetOwner); + } - ExternalTransferData filtered = content.stream().filter(t -> transferId.equals(t.getTransferId())).reduce((first, second) -> second) + private ExternalTransferData findLastTransfer(final Long loanId, final Long transferId) { + final PageExternalTransferData response = ok(() -> fineractClient.externalAssetOwners().getTransfers(Map.of("loanId", loanId))); + return response.getContent().stream().filter(t -> transferId.equals(t.getTransferId())).reduce((first, second) -> second) .orElseThrow(() -> new IllegalStateException("No element found")); + } - BigDecimal totalOutstandingBalanceAmountExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalOutstanding()); - BigDecimal outstandingPrincipalPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalPrincipalOutstanding()); - BigDecimal outstandingFeePortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalFeeChargesOutstanding()); - BigDecimal outstandingPenaltyPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalPenaltyChargesOutstanding()); - BigDecimal outstandingInterestPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalInterestOutstanding()); - BigDecimal overPaymentPortionExpected = filtered.getDetails() == null ? null - : zeroConversion(filtered.getDetails().getTotalOverpaid()); - - eventAssertion.assertEvent(LoanOwnershipTransferEvent.class, loanId).extractingData(LoanOwnershipTransferDataV1::getLoanId) - .isEqualTo(loanId).extractingData(LoanOwnershipTransferDataV1::getAssetOwnerExternalId) - .isEqualTo(filtered.getOwner().getExternalId()).extractingData(LoanOwnershipTransferDataV1::getTransferExternalId) - .isEqualTo(filtered.getTransferExternalId()).extractingData(LoanOwnershipTransferDataV1::getSettlementDate) - .isEqualTo(FORMATTER_EVENTS.format(filtered.getSettlementDate())) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getTotalOutstandingBalanceAmount) - .isEqualTo(totalOutstandingBalanceAmountExpected) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPrincipalPortion) - .isEqualTo(outstandingPrincipalPortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingFeePortion) - .isEqualTo(outstandingFeePortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPenaltyPortion) - .isEqualTo(outstandingPenaltyPortionExpected) - .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingInterestPortion) - .isEqualTo(outstandingInterestPortionExpected).extractingBigDecimal(LoanOwnershipTransferDataV1::getOverPaymentPortion) - .isEqualTo(overPaymentPortionExpected).extractingData(LoanOwnershipTransferDataV1::getType).isEqualTo(transferType) - .extractingData(LoanOwnershipTransferDataV1::getAssetOwnerExternalId).isEqualTo(assetOwner) - .extractingData(LoanOwnershipTransferDataV1::getPreviousOwnerExternalId).isEqualTo(previousAssetOwner); + private record ExpectedTransferAmounts(BigDecimal totalOutstanding, BigDecimal principal, BigDecimal fee, BigDecimal penalty, + BigDecimal interest, BigDecimal overpayment) { + + static final ExpectedTransferAmounts EMPTY = new ExpectedTransferAmounts(null, null, null, null, null, null); + } + + private ExpectedTransferAmounts expectedAmountsOf(final ExternalTransferDataDetails details) { + return new ExpectedTransferAmounts(zeroConversion(details.getTotalOutstanding()), + zeroConversion(details.getTotalPrincipalOutstanding()), zeroConversion(details.getTotalFeeChargesOutstanding()), + zeroConversion(details.getTotalPenaltyChargesOutstanding()), zeroConversion(details.getTotalInterestOutstanding()), + zeroConversion(details.getTotalOverpaid())); + } + + private EventAssertion.EventAssertionBuilder loanOwnershipTransferEventBaseCheck(final Long loanId, + final ExternalTransferData filtered, final ExpectedTransferAmounts expected) { + return eventAssertion.assertEvent(LoanOwnershipTransferEvent.class, loanId)// + .extractingData(LoanOwnershipTransferDataV1::getLoanId).isEqualTo(loanId)// + .extractingData(LoanOwnershipTransferDataV1::getAssetOwnerExternalId).isEqualTo(filtered.getOwner().getExternalId())// + .extractingData(LoanOwnershipTransferDataV1::getTransferExternalId).isEqualTo(filtered.getTransferExternalId())// + .extractingData(LoanOwnershipTransferDataV1::getSettlementDate) + .isEqualTo(FORMATTER_EVENTS.format(filtered.getSettlementDate()))// + .extractingBigDecimal(LoanOwnershipTransferDataV1::getTotalOutstandingBalanceAmount).isEqualTo(expected.totalOutstanding())// + .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPrincipalPortion).isEqualTo(expected.principal())// + .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingFeePortion).isEqualTo(expected.fee())// + .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingPenaltyPortion).isEqualTo(expected.penalty())// + .extractingBigDecimal(LoanOwnershipTransferDataV1::getOutstandingInterestPortion).isEqualTo(expected.interest())// + .extractingBigDecimal(LoanOwnershipTransferDataV1::getOverPaymentPortion).isEqualTo(expected.overpayment()); } public void loanAccountSnapshotBusinessEventCheck(Long loanId, Long transferId) { waitForTransactionCommit(); - PageExternalTransferData response = ok(() -> fineractClient.externalAssetOwners().getTransfers(Map.of("loanId", loanId))); - List content = response.getContent(); - - ExternalTransferData filtered = content.stream().filter(t -> transferId.equals(t.getTransferId())).reduce((first, second) -> second) - .orElseThrow(() -> new IllegalStateException("No element found")); + ExternalTransferData filtered = findLastTransfer(loanId, transferId); BigDecimal totalOutstandingBalanceAmountExpected = zeroConversion(filtered.getDetails().getTotalOutstanding()); BigDecimal outstandingInterestPortionExpected = zeroConversion(filtered.getDetails().getTotalInterestOutstanding()); @@ -736,4 +661,325 @@ public void approveLoanEventCheck(PostLoansLoanIdResponse loanApproveResponse) { .isEqualTo(body.getCurrency().getCode());// } + public void workingCapitalLoanCreatedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanCreatedEvent.class, loanId); + } + + public void workingCapitalLoanApplicationModifiedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanApplicationModifiedEvent.class, loanId); + } + + public void workingCapitalLoanApprovedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanApprovedEvent.class, loanId); + } + + public void workingCapitalLoanUndoApprovalEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanUndoApprovalEvent.class, loanId); + } + + public void workingCapitalLoanRejectedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanRejectedEvent.class, loanId); + } + + public void workingCapitalLoanDisbursalEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanDisbursalEvent.class, loanId); + } + + public void workingCapitalLoanUndoDisbursalEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanUndoDisbursalEvent.class, loanId); + } + + public void workingCapitalLoanStatusChangedEventCheck(final Long loanId) { + workingCapitalLoanAccountDataV1Check(WorkingCapitalLoanStatusChangedEvent.class, loanId); + } + + private GetWorkingCapitalLoansLoanIdResponse fetchWorkingCapitalLoan(final Long loanId) { + return ok(() -> fineractClient.workingCapitalLoans().retrieveWorkingCapitalLoanById(loanId)); + } + + private void workingCapitalLoanEventMatchesApiCheck(final Class eventClazz, + final Long loanId, final BiConsumer payloadAsserts) { + waitForTransactionCommit(); + final GetWorkingCapitalLoansLoanIdResponse body = fetchWorkingCapitalLoan(loanId); + eventAssertion.assertEvent(eventClazz, loanId).extractingData(event -> { + payloadAsserts.accept(event, body); + return null; + }); + } + + private void workingCapitalLoanEventPayloadCheck(final Class eventClazz, final Long loanId, + final Consumer payloadAsserts) { + waitForTransactionCommit(); + eventAssertion.assertEvent(eventClazz, loanId).extractingData(event -> { + payloadAsserts.accept(event); + return null; + }); + } + + public void workingCapitalLoanBalanceChangedEventCheck(final Long loanId) { + workingCapitalLoanEventMatchesApiCheck(WorkingCapitalLoanBalanceChangedEvent.class, loanId, (event, body) -> { + assertWorkingCapitalLoanAccountData(event, body); + + final WorkingCapitalLoanSummaryDataV1 eventSummary = event.getSummary(); + final GetWorkingCapitalLoanSummary bodySummary = body.getSummary(); + assertThat(eventSummary).isNotNull(); + assertThat(bodySummary).isNotNull(); + assertAmountEquals("summary.principalPaid", eventSummary.getPrincipalPaid(), bodySummary.getPrincipalPaid()); + assertAmountEquals("summary.principalOutstanding", eventSummary.getPrincipalOutstanding(), + bodySummary.getPrincipalOutstanding()); + assertAmountEquals("summary.totalOutstanding", eventSummary.getTotalOutstanding(), bodySummary.getTotalOutstanding()); + assertAmountEquals("totalOverpaid", event.getTotalOverpaid(), bodySummary.getOverpayment()); + }); + } + + public void workingCapitalLoanBalanceChangedEventChargesCheck(final Long loanId, final List> expectedCharges) { + workingCapitalLoanEventPayloadCheck(WorkingCapitalLoanBalanceChangedEvent.class, loanId, event -> { + final List eventCharges = event.getCharges(); + assertThat(eventCharges).isNotNull().hasSize(expectedCharges.size()); + IntStream.range(0, expectedCharges.size()).forEach(i -> { + final Map expected = expectedCharges.get(i); + final WorkingCapitalLoanChargeDataV1 actual = eventCharges.get(i); + assertAmountEquals("charges[" + i + "].amount", actual.getAmount(), new BigDecimal(expected.get("amount"))); + assertAmountEquals("charges[" + i + "].amountAccrued", actual.getAmountAccrued(), + new BigDecimal(expected.get("amountAccrued"))); + assertAmountEquals("charges[" + i + "].amountUnrecognized", actual.getAmountUnrecognized(), + new BigDecimal(expected.get("amountUnrecognized"))); + }); + }); + } + + public void workingCapitalLoanDelinquencyRangeChangeEventCheck(final Long loanId) { + workingCapitalLoanEventMatchesApiCheck(WorkingCapitalLoanDelinquencyRangeChangeEvent.class, loanId, (event, body) -> { + assertWorkingCapitalLoanAccountData(event, body); + + final WorkingCapitalLoanCollectionDataV1 eventDelinquent = event.getDelinquent(); + final WorkingCapitalCollection bodyDelinquent = body.getDelinquent(); + assertThat(eventDelinquent).isNotNull(); + assertThat(bodyDelinquent).isNotNull(); + assertAmountEquals("delinquent.delinquentAmount", eventDelinquent.getDelinquentAmount(), bodyDelinquent.getDelinquentAmount()); + assertAmountEquals("delinquent.totalDelinquentAmount", eventDelinquent.getTotalDelinquentAmount(), + bodyDelinquent.getDelinquentPrincipal()); + assertThat(eventDelinquent.getDelinquencySchedule()).isNotEmpty(); + }); + } + + public void workingCapitalLoanDelinquencyRangeChangeEventNamesRangeCheck(final Long loanId) { + workingCapitalLoanEventMatchesApiCheck(WorkingCapitalLoanDelinquencyRangeChangeEvent.class, loanId, (event, body) -> { + assertWorkingCapitalLoanAccountData(event, body); + + final WorkingCapitalLoanCollectionDataV1 eventDelinquent = event.getDelinquent(); + final WorkingCapitalCollection bodyDelinquent = body.getDelinquent(); + assertThat(eventDelinquent).isNotNull(); + assertThat(bodyDelinquent).isNotNull(); + assertAmountEquals("delinquent.delinquentAmount", eventDelinquent.getDelinquentAmount(), bodyDelinquent.getDelinquentAmount()); + assertAmountEquals("delinquent.totalDelinquentAmount", eventDelinquent.getTotalDelinquentAmount(), + bodyDelinquent.getDelinquentPrincipal()); + + final List schedule = eventDelinquent.getDelinquencySchedule(); + assertThat(schedule).as("delinquent.delinquencySchedule").isNotEmpty(); + final List eventTags = schedule.stream() + .map(WorkingCapitalLoanDelinquencySchedulePeriodDataV1::getTags).filter(Objects::nonNull).flatMap(List::stream) + .sorted(Comparator + .comparing(WorkingCapitalLoanDelinquencyScheduleTagDataV1::getRangeId, + Comparator.nullsFirst(Comparator.naturalOrder())) + .thenComparing(WorkingCapitalLoanDelinquencyScheduleTagDataV1::getDelinquentAmount, + Comparator.nullsFirst(Comparator.naturalOrder()))) + .toList(); + assertThat(eventTags).as("delinquent.delinquencySchedule[].tags").isNotEmpty(); + + final List apiTags = Optional + .ofNullable(bodyDelinquent.getInstallmentLevelDelinquency()).orElse(List.of()).stream() + .sorted(Comparator + .comparing(WorkingCapitalCollectionRangeScheduleDelinquency::getRangeId, + Comparator.nullsFirst(Comparator.naturalOrder())) + .thenComparing(WorkingCapitalCollectionRangeScheduleDelinquency::getDelinquentAmount, + Comparator.nullsFirst(Comparator.naturalOrder()))) + .toList(); + assertThat(eventTags).as("delinquent.delinquencySchedule[].tags vs API delinquent.installmentLevelDelinquency") + .hasSize(apiTags.size()); + IntStream.range(0, eventTags.size()).forEach(i -> { + final WorkingCapitalLoanDelinquencyScheduleTagDataV1 actual = eventTags.get(i); + final WorkingCapitalCollectionRangeScheduleDelinquency expected = apiTags.get(i); + assertThat(actual.getRangeId()).as("tags[%s].rangeId", i).isEqualTo(expected.getRangeId()); + assertThat(actual.getClassification()).as("tags[%s].classification", i).isNotBlank() + .isEqualTo(expected.getClassification()); + assertThat(actual.getMinimumAgeDays()).as("tags[%s].minimumAgeDays", i).isEqualTo(expected.getMinimumAgeDays()); + assertThat(actual.getMaximumAgeDays()).as("tags[%s].maximumAgeDays", i).isEqualTo(expected.getMaximumAgeDays()); + if (expected.getDelinquentAmount() == null) { + assertThat(actual.getDelinquentAmount()).as("tags[%s].delinquentAmount", i).isNull(); + } else { + assertAmountEquals("tags[" + i + "].delinquentAmount", actual.getDelinquentAmount(), expected.getDelinquentAmount()); + } + }); + }); + } + + public void workingCapitalLoanBalanceChangedEventSummaryTotalsCheck(final Long loanId, final Map expectedTotals) { + workingCapitalLoanEventPayloadCheck(WorkingCapitalLoanBalanceChangedEvent.class, loanId, event -> { + final WorkingCapitalLoanSummaryDataV1 eventSummary = event.getSummary(); + assertThat(eventSummary).isNotNull(); + expectedTotals.forEach((column, expectedValue) -> assertAmountEquals("summary." + column, summaryTotalOf(eventSummary, column), + new BigDecimal(expectedValue))); + }); + } + + private BigDecimal summaryTotalOf(final WorkingCapitalLoanSummaryDataV1 summary, final String columnName) { + return switch (columnName) { + case "totalPayoutRefund" -> summary.getTotalPayoutRefund(); + case "totalPayoutRefundReversed" -> summary.getTotalPayoutRefundReversed(); + case "totalGoodwillCredit" -> summary.getTotalGoodwillCredit(); + case "totalGoodwillCreditReversed" -> summary.getTotalGoodwillCreditReversed(); + case "totalChargeAdjustment" -> summary.getTotalChargeAdjustment(); + case "totalChargeAdjustmentReversed" -> summary.getTotalChargeAdjustmentReversed(); + case "totalCreditBalanceRefund" -> summary.getTotalCreditBalanceRefund(); + case "totalCreditBalanceRefundReversed" -> summary.getTotalCreditBalanceRefundReversed(); + case "totalRepaymentTransaction" -> summary.getTotalRepaymentTransaction(); + case "totalRepaymentTransactionReversed" -> summary.getTotalRepaymentTransactionReversed(); + default -> throw new IllegalArgumentException("Unsupported summary transaction type total column: " + columnName); + }; + } + + public void workingCapitalLoanBalanceChangedEventChargesWithoutAccrualCheck(final Long loanId) { + workingCapitalLoanEventPayloadCheck(WorkingCapitalLoanBalanceChangedEvent.class, loanId, event -> { + final List eventCharges = event.getCharges(); + assertThat(eventCharges).isNotNull().isNotEmpty(); + eventCharges.forEach(charge -> { + assertThat(charge.getAmountAccrued()).as("amountAccrued of charge %s", charge.getId()).isNull(); + assertThat(charge.getAmountUnrecognized()).as("amountUnrecognized of charge %s", charge.getId()).isNull(); + }); + }); + } + + public void workingCapitalLoanDisbursalEventDeepPayloadCheck(final Long loanId) { + workingCapitalLoanEventMatchesApiCheck(WorkingCapitalLoanDisbursalEvent.class, loanId, (event, body) -> { + assertWorkingCapitalLoanAccountData(event, body); + assertAmountEquals("principal", event.getPrincipal(), body.getPrincipal()); + assertAmountEquals("approvedPrincipal", event.getApprovedPrincipal(), body.getApprovedPrincipal()); + assertAmountEquals("netDisbursalAmount", event.getNetDisbursalAmount(), body.getNetDisbursalAmount()); + assertWorkingCapitalLoanTimelineData(event.getTimeline(), body.getTimeline()); + assertWorkingCapitalLoanBreachData(event.getBreach(), body); + assertWorkingCapitalLoanDelinquencyData(event.getDelinquency(), body); + }); + } + + private void assertWorkingCapitalLoanTimelineData(final LoanApplicationTimelineDataV1 eventTimeline, + final GetWorkingCapitalLoansLoanIdTimeline bodyTimeline) { + assertThat(eventTimeline).isNotNull(); + assertThat(bodyTimeline).isNotNull(); + assertEventDateEqualsApiDate("timeline.submittedOnDate", eventTimeline.getSubmittedOnDate(), bodyTimeline.getSubmittedOnDate()); + assertEventDateEqualsApiDate("timeline.approvedOnDate", eventTimeline.getApprovedOnDate(), bodyTimeline.getApprovedOnDate()); + assertEventDateEqualsApiDate("timeline.expectedDisbursementDate", eventTimeline.getExpectedDisbursementDate(), + bodyTimeline.getExpectedDisbursementDate()); + assertEventDateEqualsApiDate("timeline.actualDisbursementDate", eventTimeline.getActualDisbursementDate(), + bodyTimeline.getActualDisbursementDate()); + assertEventDateEqualsApiDate("timeline.expectedMaturityDate", eventTimeline.getExpectedMaturityDate(), + bodyTimeline.getExpectedMaturityDate()); + assertEventDateEqualsApiDate("timeline.closedOnDate", eventTimeline.getClosedOnDate(), bodyTimeline.getClosedOnDate()); + assertThat(eventTimeline.getSubmittedByUsername()).as("timeline.submittedByUsername") + .isEqualTo(bodyTimeline.getSubmittedByUsername()); + assertThat(eventTimeline.getApprovedByUsername()).as("timeline.approvedByUsername").isEqualTo(bodyTimeline.getApprovedByUsername()); + assertThat(eventTimeline.getDisbursedByUsername()).as("timeline.disbursedByUsername") + .isEqualTo(bodyTimeline.getDisbursedByUsername()); + } + + private void assertWorkingCapitalLoanBreachData(final WorkingCapitalBreachDataV1 eventBreach, + final GetWorkingCapitalLoansLoanIdResponse body) { + if (body.getBreach() == null && body.getBreachGraceDays() == null && body.getBreachStartDate() == null) { + return; + } + assertThat(eventBreach).as("breach").isNotNull(); + Optional.ofNullable(body.getBreach()).ifPresent(bodyBreach -> { + assertThat(eventBreach.getId()).as("breach.id").isEqualTo(bodyBreach.getId()); + assertThat(eventBreach.getName()).as("breach.name").isEqualTo(bodyBreach.getName()); + assertThat(eventBreach.getBreachFrequency()).as("breach.breachFrequency").isEqualTo(bodyBreach.getBreachFrequency()); + assertAmountEquals("breach.breachAmount", eventBreach.getBreachAmount(), bodyBreach.getBreachAmount()); + }); + assertThat(eventBreach.getBreachGraceDays()).as("breach.breachGraceDays").isEqualTo(body.getBreachGraceDays()); + assertEventDateEqualsApiDate("breach.breachStartDate", eventBreach.getBreachStartDate(), body.getBreachStartDate()); + Optional.ofNullable(body.getNearBreach()).ifPresent(bodyNearBreach -> { + assertThat(eventBreach.getNearBreach()).as("breach.nearBreach").isNotNull(); + assertThat(eventBreach.getNearBreach().getFrequency()).as("breach.nearBreach.frequency") + .isEqualTo(bodyNearBreach.getFrequency()); + assertAmountEquals("breach.nearBreach.threshold", eventBreach.getNearBreach().getThreshold(), bodyNearBreach.getThreshold()); + }); + } + + private void assertWorkingCapitalLoanDelinquencyData(final WorkingCapitalLoanDelinquencyDataV1 eventDelinquency, + final GetWorkingCapitalLoansLoanIdResponse body) { + if (body.getDelinquencyStartType() == null && body.getDelinquencyStartDate() == null) { + return; + } + assertThat(eventDelinquency).as("delinquency").isNotNull(); + Optional.ofNullable(body.getDelinquencyStartType()).ifPresent(bodyStartType -> { + assertThat(eventDelinquency.getDelinquencyStartType()).as("delinquency.delinquencyStartType").isNotNull(); + assertThat(eventDelinquency.getDelinquencyStartType().getCode()).as("delinquency.delinquencyStartType.code") + .isEqualTo(bodyStartType.getCode()); + }); + assertEventDateEqualsApiDate("delinquency.delinquencyStartDate", eventDelinquency.getDelinquencyStartDate(), + body.getDelinquencyStartDate()); + } + + public void workingCapitalLoanStatusChangedEventTimelineCheck(final Long loanId) { + workingCapitalLoanEventMatchesApiCheck(WorkingCapitalLoanStatusChangedEvent.class, loanId, (event, body) -> { + assertWorkingCapitalLoanAccountData(event, body); + assertWorkingCapitalLoanTimelineData(event.getTimeline(), body.getTimeline()); + }); + } + + public void workingCapitalLoanBalanceChangedEventPausePeriodsCheck(final Long loanId) { + workingCapitalLoanEventMatchesApiCheck(WorkingCapitalLoanBalanceChangedEvent.class, loanId, (event, body) -> { + assertThat(event.getDelinquent()).isNotNull(); + final List eventPausePeriods = event.getDelinquent().getDelinquencyPausePeriods(); + assertThat(eventPausePeriods).isNotNull().isNotEmpty(); + assertThat(body.getDelinquent()).isNotNull(); + final List bodyPausePeriods = body.getDelinquent().getDelinquencyPausePeriods(); + assertThat(bodyPausePeriods).isNotNull().hasSize(eventPausePeriods.size()); + IntStream.range(0, eventPausePeriods.size()).forEach(i -> { + final DelinquencyPausePeriodV1 actual = eventPausePeriods.get(i); + final WorkingCapitalCollectionDelinquencyPausePeriod expected = bodyPausePeriods.get(i); + assertThat(actual.getActive()).as("delinquencyPausePeriods[%s].active", i).isEqualTo(expected.getActive()); + assertEventDateEqualsApiDate("delinquencyPausePeriods[" + i + "].pausePeriodStart", actual.getPausePeriodStart(), + expected.getPausePeriodStart()); + assertEventDateEqualsApiDate("delinquencyPausePeriods[" + i + "].pausePeriodEnd", actual.getPausePeriodEnd(), + expected.getPausePeriodEnd()); + }); + }); + } + + public void workingCapitalLoanStatusChangedEventNotRaisedCheck(final Long loanId) { + waitForTransactionCommit(); + eventAssertion.assertEventNotRaised(WorkingCapitalLoanStatusChangedEvent.class, loanId); + } + + public void workingCapitalLoanBalanceChangedEventNotRaisedCheck(final Long loanId) { + waitForTransactionCommit(); + eventAssertion.assertEventNotRaised(WorkingCapitalLoanBalanceChangedEvent.class, loanId); + } + + private static void assertEventDateEqualsApiDate(final String description, final String eventDate, final LocalDate apiDate) { + assertThat(eventDate).as(description).isEqualTo(apiDate == null ? null : FORMATTER_EVENTS.format(apiDate)); + } + + private void workingCapitalLoanAccountDataV1Check(final Class eventClazz, + final Long loanId) { + workingCapitalLoanEventMatchesApiCheck(eventClazz, loanId, this::assertWorkingCapitalLoanAccountData); + } + + private void assertWorkingCapitalLoanAccountData(final WorkingCapitalLoanAccountDataV1 event, + final GetWorkingCapitalLoansLoanIdResponse body) { + assertThat(body.getStatus()).isNotNull(); + assertThat(event.getStatus().getId().longValue()).as("status.id").isEqualTo(body.getStatus().getId()); + + assertThat(event.getDelinquent()).isNotNull(); + assertThat(event.getDelinquent().getDelinquencySchedule()).isNotNull(); + + assertThat(event.getBreach()).isNotNull(); + assertThat(event.getBreach().getBreachSchedule()).isNotNull(); + + if (event.getDisbursementDetails() != null && !event.getDisbursementDetails().isEmpty() && body.getNetDisbursalAmount() != null) { + event.getDisbursementDetails().forEach(tranche -> assertAmountEquals("disbursementDetails.netDisbursalAmount", + tranche.getNetDisbursalAmount(), body.getNetDisbursalAmount())); + } + } + } diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/AbstractWorkingCapitalLoanEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/AbstractWorkingCapitalLoanEvent.java new file mode 100644 index 00000000000..d4d9d48d041 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/AbstractWorkingCapitalLoanEvent.java @@ -0,0 +1,36 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +import java.util.function.Function; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.test.messaging.event.Event; + +public abstract class AbstractWorkingCapitalLoanEvent implements Event { + + @Override + public Class getDataClass() { + return WorkingCapitalLoanAccountDataV1.class; + } + + @Override + public Function getIdExtractor() { + return WorkingCapitalLoanAccountDataV1::getId; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedEvent.java new file mode 100644 index 00000000000..914baf500bb --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanApplicationModifiedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanApplicationModifiedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApprovedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApprovedEvent.java new file mode 100644 index 00000000000..289ce9dccf1 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanApprovedEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanApprovedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanApprovedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedEvent.java new file mode 100644 index 00000000000..1005fbd82fb --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanBalanceChangedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanBalanceChangedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanCreatedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanCreatedEvent.java new file mode 100644 index 00000000000..6b3817dc820 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanCreatedEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanCreatedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanCreatedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeEvent.java new file mode 100644 index 00000000000..219ef798af6 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanDelinquencyRangeChangeEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDisbursalEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDisbursalEvent.java new file mode 100644 index 00000000000..99ed3ad59a0 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanDisbursalEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanDisbursalEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanDisbursalBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanRejectedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanRejectedEvent.java new file mode 100644 index 00000000000..f64208516c9 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanRejectedEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanRejectedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanRejectedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedEvent.java new file mode 100644 index 00000000000..bcf4a4baecd --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanStatusChangedEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanStatusChangedBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalEvent.java new file mode 100644 index 00000000000..f008572405c --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanUndoApprovalEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanUndoApprovalBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalEvent.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalEvent.java new file mode 100644 index 00000000000..ff2b0802441 --- /dev/null +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalEvent.java @@ -0,0 +1,27 @@ +/** + * 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.messaging.event.workingcapitalloan.loan; + +public class WorkingCapitalLoanUndoDisbursalEvent extends AbstractWorkingCapitalLoanEvent { + + @Override + public String getEventName() { + return "WorkingCapitalLoanUndoDisbursalBusinessEvent"; + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java index bb07efd8b7f..9883024505c 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/WorkingCapitalLoanAccountStepDef.java @@ -1446,6 +1446,101 @@ public void checkWorkingCapitalDiscountFeeAdjustmentBusinessEventIsRaised(final new BigDecimal(amount), transactionDate); } + @Then("a Working Capital Loan Created business event is raised") + public void aWorkingCapitalLoanCreatedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanCreatedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Application Modified business event is raised") + public void aWorkingCapitalLoanApplicationModifiedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanApplicationModifiedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Approved business event is raised") + public void aWorkingCapitalLoanApprovedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanApprovedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Undo Approval business event is raised") + public void aWorkingCapitalLoanUndoApprovalBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanUndoApprovalEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Rejected business event is raised") + public void aWorkingCapitalLoanRejectedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanRejectedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Disbursal business event is raised") + public void aWorkingCapitalLoanDisbursalBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanDisbursalEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Undo Disbursal business event is raised") + public void aWorkingCapitalLoanUndoDisbursalBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanUndoDisbursalEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Status Changed business event is raised") + public void aWorkingCapitalLoanStatusChangedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanStatusChangedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Balance Changed business event is raised") + public void aWorkingCapitalLoanBalanceChangedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanBalanceChangedEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Balance Changed business event is raised with charges:") + public void aWorkingCapitalLoanBalanceChangedBusinessEventIsRaisedWithCharges(final DataTable table) { + eventCheckHelper.workingCapitalLoanBalanceChangedEventChargesCheck(getCreatedLoanId(), table.asMaps()); + } + + @Then("a Working Capital Loan Delinquency Range Change business event is raised") + public void aWorkingCapitalLoanDelinquencyRangeChangeBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanDelinquencyRangeChangeEventCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Delinquency Range Change business event is raised naming the delinquency range") + public void aWorkingCapitalLoanDelinquencyRangeChangeBusinessEventIsRaisedNamingTheDelinquencyRange() { + eventCheckHelper.workingCapitalLoanDelinquencyRangeChangeEventNamesRangeCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Balance Changed business event is raised with transaction type totals:") + public void aWorkingCapitalLoanBalanceChangedBusinessEventIsRaisedWithTransactionTypeTotals(final DataTable table) { + eventCheckHelper.workingCapitalLoanBalanceChangedEventSummaryTotalsCheck(getCreatedLoanId(), table.asMaps().get(0)); + } + + @Then("a Working Capital Loan Balance Changed business event is raised where charge accrual fields are not populated") + public void aWorkingCapitalLoanBalanceChangedBusinessEventIsRaisedWithoutChargeAccrualFields() { + eventCheckHelper.workingCapitalLoanBalanceChangedEventChargesWithoutAccrualCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Disbursal business event is raised with loan details matching the API") + public void aWorkingCapitalLoanDisbursalBusinessEventIsRaisedWithLoanDetailsMatchingTheApi() { + eventCheckHelper.workingCapitalLoanDisbursalEventDeepPayloadCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Status Changed business event is raised with timeline matching the API") + public void aWorkingCapitalLoanStatusChangedBusinessEventIsRaisedWithTimelineMatchingTheApi() { + eventCheckHelper.workingCapitalLoanStatusChangedEventTimelineCheck(getCreatedLoanId()); + } + + @Then("a Working Capital Loan Balance Changed business event is raised with the delinquency pause periods") + public void aWorkingCapitalLoanBalanceChangedBusinessEventIsRaisedWithDelinquencyPausePeriods() { + eventCheckHelper.workingCapitalLoanBalanceChangedEventPausePeriodsCheck(getCreatedLoanId()); + } + + @Then("no Working Capital Loan Status Changed business event is raised") + public void noWorkingCapitalLoanStatusChangedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanStatusChangedEventNotRaisedCheck(getCreatedLoanId()); + } + + @Then("no Working Capital Loan Balance Changed business event is raised") + public void noWorkingCapitalLoanBalanceChangedBusinessEventIsRaised() { + eventCheckHelper.workingCapitalLoanBalanceChangedEventNotRaisedCheck(getCreatedLoanId()); + } + @Then("Admin fails to disburse the Working Capital loan on {string} with {string} EUR transaction amount because of not approved") public void disburseWCLoanFailureWithNotApproved(String actualDisbursementDate, String transactionAmount) { final PostWorkingCapitalLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanAccountBusinessEvents.feature b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanAccountBusinessEvents.feature new file mode 100644 index 00000000000..191b108cad9 --- /dev/null +++ b/fineract-e2e-tests-runner/src/test/resources/features/WorkingCapitalLoanAccountBusinessEvents.feature @@ -0,0 +1,379 @@ +@WorkingCapital +@WorkingCapitalLoanAccountBusinessEventsFeature +@SerialChargeAccrualConfig +Feature: Working Capital Loan Account Business Events + + @TestRailId:C89791 + Scenario: Working Capital loan raises Created, Application Modified, Approved, Undo Approval, Disbursal and Undo Disbursal business events across its lifecycle + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100.0 | 100.0 | 1.0 | 0.0 | + Then Working capital loan creation was successful + And a Working Capital Loan Created business event is raised + When Admin modifies the working capital loan with the following data: + | submittedOnDate | expectedDisbursementDate | principalAmount | totalPaymentVolume | periodPaymentRate | discount | + | | | 80.0 | | | | + Then a Working Capital Loan Application Modified business event is raised + And no Working Capital Loan Status Changed business event is raised + When Admin successfully approves the working capital loan on "01 January 2026" with "80" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And a Working Capital Loan Approved business event is raised + And a Working Capital Loan Status Changed business event is raised + And no Working Capital Loan Balance Changed business event is raised + When Admin makes undo approval on the working capital loan + Then Working capital loan undo approval was successful + And a Working Capital Loan Undo Approval business event is raised + And a Working Capital Loan Status Changed business event is raised + When Admin successfully approves the working capital loan on "01 January 2026" with "80" amount and expected disbursement date on "01 January 2026" + Then Working capital loan approval was successful + And a Working Capital Loan Approved business event is raised + And a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "80" EUR transaction amount + Then a Working Capital Loan Disbursal business event is raised with loan details matching the API + And a Working Capital Loan Status Changed business event is raised + When Admin successfully undo Working Capital disbursal + Then a Working Capital Loan Undo Disbursal business event is raised + And a Working Capital Loan Status Changed business event is raised + + @TestRailId:C89792 + Scenario: Rejected Working Capital loan raises Rejected and Status Changed business events + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100.0 | 100.0 | 1.0 | 0.0 | + Then Working capital loan creation was successful + When Admin rejects the working capital loan on "01 January 2026" + Then Working capital loan rejection was successful + And a Working Capital Loan Rejected business event is raised + And a Working Capital Loan Status Changed business event is raised + + @TestRailId:C89793 + Scenario: Working Capital loan raises Balance Changed on partial repayment and Status Changed on full repayment + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "10 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Customer makes repayment on "10 January 2026" with 4000.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + And no Working Capital Loan Status Changed business event is raised + When Admin sets the business date to "20 January 2026" + And Customer makes repayment on "20 January 2026" with 5000.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised with timeline matching the API + And a Working Capital Loan Balance Changed business event is raised with transaction type totals: + | totalRepaymentTransaction | totalRepaymentTransactionReversed | + | 9000.0 | 0.0 | + + @TestRailId:C89794 + Scenario: Overpaid Working Capital loan raises Balance Changed and Status Changed business events through credit balance refund + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "20 January 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + And Customer makes repayment on "20 January 2026" with 9200.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "OVERPAID" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Customer makes credit balance refund on "20 January 2026" with 200.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised with transaction type totals: + | totalRepaymentTransaction | totalCreditBalanceRefund | totalCreditBalanceRefundReversed | + | 9200.0 | 200.0 | 0.0 | + + @TestRailId:C89795 + Scenario: Delinquent Working Capital loan raises a Delinquency Range Change business event + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And Admin runs inline COB job for Working Capital Loan by loanId + When Admin sets the business date to "15 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then a Working Capital Loan Delinquency Range Change business event is raised naming the delinquency range + When Admin closes the Working Capital loan with a full repayment on "15 February 2026" + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + When Admin sets the business date to "16 February 2026" + And Admin runs inline COB job for Working Capital Loan by loanId + Then a Working Capital Loan Delinquency Range Change business event is raised + + @TestRailId:C89796 + Scenario: Working Capital loan raises Balance Changed on discount fee, discount fee adjustment and undo of the adjustment + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin adds Discount fee with "12" amount on Working Capital loan account for last disbursement + Then a Working Capital Loan Balance Changed business event is raised + When Admin adds Discount fee adjustment with "5" amount on Working Capital loan account for last discount + Then a Working Capital Loan Balance Changed business event is raised + When Admin undo the last Discount fee adjustment on Working Capital loan account + Then a Working Capital Loan Balance Changed business event is raised + When Admin closes the Working Capital loan with a full repayment on "01 January 2026" + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + + @TestRailId:C89797 + Scenario: Working Capital loan Balance Changed event exposes charge amountAccrued and amountUnrecognized before and after accrual + Given Admin sets the business date to "01 January 2028" + And Admin creates a client with random data and creates-approves-disburses a working capital loan with the following data: + | LoanProduct | submittedOnDate | expectedDisbursementDate | principalAmount | totalPayment | periodPaymentRate | discount | + | WCLP_ACC_DEF_REV_AM | 01 January 2028 | 01 January 2028 | 9000 | 100000 | 18 | 0 | + And a Working Capital Loan Balance Changed business event is raised + When Global config "charge-accrual-date" value set to "due-date" + And Admin sets the business date to "05 January 2028" + And Admin runs inline COB job for Working Capital Loan by loanId + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 January 2028" due date and 50.0 transaction amount + And a Working Capital Loan Balance Changed business event is raised + And Admin sets the business date to "10 January 2028" + And Customer makes repayment on "10 January 2028" with 100.0 transaction amount on Working Capital loan + Then a Working Capital Loan Balance Changed business event is raised with charges: + | amount | amountAccrued | amountUnrecognized | + | 50.0 | 0.0 | 50.0 | + When Admin sets the business date to "16 January 2028" + And Admin runs inline COB job for Working Capital Loan by loanId + Then Admin closes the Working Capital loan with a full repayment on "16 January 2028" + And a Working Capital Loan Balance Changed business event is raised with charges: + | amount | amountAccrued | amountUnrecognized | + | 50.0 | 50.0 | 0.0 | + + @TestRailId:C89798 + Scenario: Working Capital loan Balance Changed events carry payout refund, goodwill credit and reversal totals in the summary + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "05 January 2026" + And Customer makes "PAYOUT_REFUND" transaction on "05 January 2026" with 1000.0 transaction amount on Working Capital loan + Then a Working Capital Loan Balance Changed business event is raised with transaction type totals: + | totalPayoutRefund | totalPayoutRefundReversed | totalGoodwillCredit | totalRepaymentTransaction | + | 1000.0 | 0.0 | 0.0 | 0.0 | + When Customer makes "GOODWILL_CREDIT" transaction on "05 January 2026" with 500.0 transaction amount on Working Capital loan + Then a Working Capital Loan Balance Changed business event is raised with transaction type totals: + | totalPayoutRefund | totalGoodwillCredit | totalGoodwillCreditReversed | + | 1000.0 | 500.0 | 0.0 | + When Customer undo "1"th "PAYOUT_REFUND" transaction made on "05 January 2026" on Working Capital loan + Then a Working Capital Loan Balance Changed business event is raised with transaction type totals: + | totalPayoutRefund | totalPayoutRefundReversed | totalGoodwillCredit | + | 0.0 | 1000.0 | 500.0 | + When Admin closes the Working Capital loan with a full repayment on "05 January 2026" + + @TestRailId:C89799 + Scenario: Undoing a closing repayment reopens the Working Capital loan and raises Status Changed and Balance Changed business events + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "10 January 2026" + And Customer makes repayment on "10 January 2026" with 100.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised with timeline matching the API + And a Working Capital Loan Balance Changed business event is raised with transaction type totals: + | totalRepaymentTransaction | totalRepaymentTransactionReversed | + | 100.0 | 0.0 | + When Customer undo "1"th "REPAYMENT" transaction made on "10 January 2026" on Working Capital loan + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised with transaction type totals: + | totalRepaymentTransaction | totalRepaymentTransactionReversed | + | 0.0 | 100.0 | + When Admin closes the Working Capital loan with a full repayment on "10 January 2026" + + @TestRailId:C89800 + Scenario: Working Capital loan Balance Changed event exposes the delinquency pause periods + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + When Admin runs inline COB job for Working Capital Loan by loanId + And Admin initiate a Working Capital loan delinquency pause with startDate "01 January 2026" and endDate "15 January 2026" + And Admin sets the business date to "06 January 2026" + And Customer makes repayment on "06 January 2026" with 100.0 transaction amount on Working Capital loan + Then a Working Capital Loan Balance Changed business event is raised with the delinquency pause periods + When Admin closes the Working Capital loan with a full repayment on "06 January 2026" + + @TestRailId:C89801 + Scenario: Working Capital loan Balance Changed event leaves charge accrual fields empty for products without deferred revenue accrual accounting + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + And Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + When Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 January 2026" due date and 50.0 transaction amount + And Admin sets the business date to "10 January 2026" + And Customer makes repayment on "10 January 2026" with 100.0 transaction amount on Working Capital loan + Then a Working Capital Loan Balance Changed business event is raised where charge accrual fields are not populated + When Admin closes the Working Capital loan with a full repayment on "10 January 2026" + + @TestRailId:C89802 + Scenario: Adding a charge to a closed Working Capital loan reopens it and raises Status Changed and Balance Changed business events + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "05 January 2026" + And Customer makes repayment on "05 January 2026" with 100.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "10 January 2026" + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 January 2026" due date and 50.0 transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin closes the Working Capital loan with a full repayment on "10 January 2026" + + @TestRailId:C89803 + Scenario: A charge adjustment that settles the outstanding charge closes the Working Capital loan and raises Status Changed and Balance Changed business events + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "05 January 2026" + And Customer makes repayment on "05 January 2026" with 100.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "10 January 2026" + And Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "20 January 2026" due date and 50.0 transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin makes a charge adjustment for the last added charge with 50.0 amount on working capital loan + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + + @TestRailId:C89804 + Scenario: A partial charge adjustment on an active Working Capital loan raises Balance Changed without Status Changed + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 9000 | 100000 | 18 | 0 | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "9000" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "9000" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin adds "WORKING_CAPITAL_SPECIFIED_DUE_DATE_FEE" specified due date charge to working capital loan with "15 January 2026" due date and 50.0 transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + And no Working Capital Loan Status Changed business event is raised + When Admin makes a charge adjustment for the last added charge with 20.0 amount on working capital loan + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + And no Working Capital Loan Status Changed business event is raised + When Admin closes the Working Capital loan with a full repayment on "01 January 2026" + + @TestRailId:C89805 + Scenario: Undoing a discount fee adjustment that closed the Working Capital loan reopens it and raises Status Changed and Balance Changed business events + 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 | totalPaymentVolume | periodPaymentRate | discount | + | WCLP | 01 January 2026 | 01 January 2026 | 100 | 100 | 1 | | + Then Working capital loan creation was successful + When Admin successfully approves the working capital loan on "01 January 2026" with "100" amount and expected disbursement date on "01 January 2026" + Then a Working Capital Loan Status Changed business event is raised + When Admin successfully disburse the Working Capital loan on "01 January 2026" with "100" EUR transaction amount + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin adds Discount fee with "12" amount on Working Capital loan account for last disbursement + Then a Working Capital Loan Balance Changed business event is raised + When Admin sets the business date to "05 January 2026" + And Customer makes repayment on "05 January 2026" with 100.0 transaction amount on Working Capital loan + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Balance Changed business event is raised + When Admin adds Discount fee adjustment with "12" amount on Working Capital loan account for last discount + Then Working Capital loan status will be "CLOSED_OBLIGATIONS_MET" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised + When Admin undo the last Discount fee adjustment on Working Capital loan account + Then Working Capital loan status will be "ACTIVE" + And a Working Capital Loan Status Changed business event is raised + And a Working Capital Loan Balance Changed business event is raised diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java index 78673e840f1..4e6d1679f5b 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java @@ -117,16 +117,21 @@ public void givenAllConfigurationWhenValidatedThenValidationSuccessful() throws "WorkingCapitalLoanDisbursalTransactionBusinessEvent", "WorkingCapitalLoanUndoDisbursalTransactionBusinessEvent", "WorkingCapitalLoanRepaymentTransactionBusinessEvent", "WorkingCapitalLoanDiscountFeeTransactionBusinessEvent", "WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent", "WorkingCapitalLoanChargeAdjustmentPreBusinessEvent", - "WorkingCapitalLoanChargeAdjustmentPostBusinessEvent", "WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent"); + "WorkingCapitalLoanChargeAdjustmentPostBusinessEvent", "WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent", + "WorkingCapitalLoanCreatedBusinessEvent", "WorkingCapitalLoanApplicationModifiedBusinessEvent", + "WorkingCapitalLoanApprovedBusinessEvent", "WorkingCapitalLoanUndoApprovalBusinessEvent", + "WorkingCapitalLoanRejectedBusinessEvent", "WorkingCapitalLoanDisbursalBusinessEvent", + "WorkingCapitalLoanUndoDisbursalBusinessEvent", "WorkingCapitalLoanStatusChangedBusinessEvent", + "WorkingCapitalLoanBalanceChangedBusinessEvent", "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"); - List tenants = Arrays - .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); + List tenants = List + .of(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null)); JdbcTemplate jdbcTemplate = Mockito.mock(JdbcTemplate.class); when(tenantDetailsService.findAllTenants()).thenReturn(tenants); when(jdbcTemplateFactory.create(any())).thenReturn(jdbcTemplate); when(jdbcTemplate.queryForList(anyString(), eq(String.class))).thenReturn(configurations); - List sourcePackage = Arrays.asList("org.apache.fineract"); + List sourcePackage = List.of("org.apache.fineract"); when(externalEventSourceService.getSourcePackages()).thenReturn(sourcePackage); // when underTest.afterPropertiesSet(); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedBusinessEvent.java new file mode 100644 index 00000000000..137338c1823 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApplicationModifiedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanApplicationModifiedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanApplicationModifiedBusinessEvent"; + + public WorkingCapitalLoanApplicationModifiedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApprovedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApprovedBusinessEvent.java new file mode 100644 index 00000000000..09dda04b015 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanApprovedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanApprovedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanApprovedBusinessEvent"; + + public WorkingCapitalLoanApprovedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedBusinessEvent.java new file mode 100644 index 00000000000..3042f352d9a --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBalanceChangedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanBalanceChangedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanBalanceChangedBusinessEvent"; + + public WorkingCapitalLoanBalanceChangedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBusinessEvent.java new file mode 100644 index 00000000000..cb43af49c2d --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanBusinessEvent.java @@ -0,0 +1,41 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.infrastructure.event.business.domain.AbstractBusinessEvent; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public abstract class WorkingCapitalLoanBusinessEvent extends AbstractBusinessEvent { + + private static final String CATEGORY = "WorkingCapitalLoan"; + + public WorkingCapitalLoanBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Long getAggregateRootId() { + return get().getId(); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanCreatedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanCreatedBusinessEvent.java new file mode 100644 index 00000000000..ec3b61b8d8c --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanCreatedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanCreatedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanCreatedBusinessEvent"; + + public WorkingCapitalLoanCreatedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent.java new file mode 100644 index 00000000000..823a960f076 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"; + + public WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDisbursalBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDisbursalBusinessEvent.java new file mode 100644 index 00000000000..ddbcbe70ccd --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanDisbursalBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanDisbursalBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanDisbursalBusinessEvent"; + + public WorkingCapitalLoanDisbursalBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanRejectedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanRejectedBusinessEvent.java new file mode 100644 index 00000000000..6699dabeb2f --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanRejectedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanRejectedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanRejectedBusinessEvent"; + + public WorkingCapitalLoanRejectedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedBusinessEvent.java new file mode 100644 index 00000000000..9a6119b0fed --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanStatusChangedBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanStatusChangedBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanStatusChangedBusinessEvent"; + + public WorkingCapitalLoanStatusChangedBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalBusinessEvent.java new file mode 100644 index 00000000000..73a6626b1a6 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoApprovalBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanUndoApprovalBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanUndoApprovalBusinessEvent"; + + public WorkingCapitalLoanUndoApprovalBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalBusinessEvent.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalBusinessEvent.java new file mode 100644 index 00000000000..f19eedda7cd --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/workingcapitalloan/loan/WorkingCapitalLoanUndoDisbursalBusinessEvent.java @@ -0,0 +1,35 @@ +/** + * 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.infrastructure.event.business.domain.workingcapitalloan.loan; + +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; + +public class WorkingCapitalLoanUndoDisbursalBusinessEvent extends WorkingCapitalLoanBusinessEvent { + + private static final String TYPE = "WorkingCapitalLoanUndoDisbursalBusinessEvent"; + + public WorkingCapitalLoanUndoDisbursalBusinessEvent(final WorkingCapitalLoan value) { + super(value); + } + + @Override + public String getType() { + return TYPE; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ChargeIdAndAmountHolder.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ChargeIdAndAmountHolder.java new file mode 100644 index 00000000000..45e32505b6b --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/ChargeIdAndAmountHolder.java @@ -0,0 +1,24 @@ +/** + * 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; + +public record ChargeIdAndAmountHolder(Long chargeId, BigDecimal amount) { +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/TransactionTypeTotalHolder.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/TransactionTypeTotalHolder.java new file mode 100644 index 00000000000..cd3521b7196 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/data/TransactionTypeTotalHolder.java @@ -0,0 +1,25 @@ +/** + * 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 org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; + +public record TransactionTypeTotalHolder(LoanTransactionType transactionType, boolean reversed, BigDecimal total) { +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java index 32929b76b36..4b5b8009f44 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/domain/WorkingCapitalLoanTransactionRelationRepository.java @@ -22,8 +22,11 @@ import java.util.List; import java.util.Optional; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.workingcapitalloan.data.ChargeIdAndAmountHolder; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository @@ -38,4 +41,16 @@ List findAllByToTransactionAndFromTransac List findAllByToChargeAndFromTransactionReversedAndFromTransactionTransactionType( WorkingCapitalLoanCharge toCharge, boolean reversed, LoanTransactionType transactionType); + + @Query(""" + SELECT r.toCharge.id, SUM(r.fromTransaction.transactionAmount) + FROM WorkingCapitalLoanTransactionRelation r + WHERE r.fromTransaction.wcLoan.id = :wcLoanId + AND r.fromTransaction.reversed = FALSE + AND r.fromTransaction.transactionType = :transactionType + AND r.toCharge IS NOT NULL + GROUP BY r.toCharge.id + """) + List fetchTransactionAmountPerCharge(@Param("wcLoanId") Long wcLoanId, + @Param("transactionType") LoanTransactionType transactionType); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java index 5d5a416469f..b6dc3fc7c46 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanDelinquencyActionRepository.java @@ -35,6 +35,9 @@ public interface WorkingCapitalLoanDelinquencyActionRepository extends JpaReposi List findByWorkingCapitalLoanIdAndActionOrderByIdDesc(Long workingCapitalLoanId, DelinquencyAction action); + List findByWorkingCapitalLoanIdAndActionOrderByStartDateAsc(Long workingCapitalLoanId, + DelinquencyAction action); + @Query(""" select case when count(action) > 0 then true else false end from WorkingCapitalLoanDelinquencyAction action where action.action = org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.DISABLE diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java index 9128760fc4c..a73f66ec97c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/repository/WorkingCapitalLoanTransactionRepository.java @@ -26,6 +26,7 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.workingcapitalloan.data.TransactionDateAndAmountHolder; +import org.apache.fineract.portfolio.workingcapitalloan.data.TransactionTypeTotalHolder; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -105,4 +106,24 @@ SELECT CASE WHEN COUNT(transaction) > 0 THEN TRUE ELSE FALSE END """) boolean existsLaterTransaction(@Param("loanId") Long loanId, @Param("transactionDate") LocalDate transactionDate, @Param("createdDateTime") OffsetDateTime createdDateTime); + + @Query(""" + SELECT t.transactionType, t.reversed, SUM(t.transactionAmount) + FROM WorkingCapitalLoanTransaction t + WHERE t.wcLoan.id = :wcLoanId + AND t.transactionType in :transactionTypes + GROUP BY t.transactionType, t.reversed + """) + List fetchTotalsPerTypeAndReversed(@Param("wcLoanId") Long wcLoanId, + @Param("transactionTypes") List transactionTypes); + + @Query(""" + SELECT t FROM WorkingCapitalLoanTransaction t + WHERE t.wcLoan.id = :wcLoanId + AND t.reversed = FALSE + AND t.transactionType in :transactionTypes + ORDER BY t.transactionDate DESC, t.id DESC + """) + List findActiveByTypesOrderByDateDesc(@Param("wcLoanId") Long wcLoanId, + @Param("transactionTypes") List transactionTypes); } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java index 604f5944d9e..37f7804ed7d 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/WorkingCapitalLoanDataValidator.java @@ -275,9 +275,11 @@ public void validateUndoDiscountAdjustmentTransaction(final WorkingCapitalLoan l baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.loanProductRelatedDetailsParamName) .failWithCode("discount.not.available"); } - if (loan.getLoanStatus() == null || !loan.getLoanStatus().isActive()) { + final LoanStatus loanStatus = loan.getLoanStatus(); + final boolean undoAllowedForStatus = LoanStatus.ACTIVE.equals(loanStatus) || LoanStatus.CLOSED_OBLIGATIONS_MET.equals(loanStatus); + if (!undoAllowedForStatus) { baseDataValidator.reset().parameter(WorkingCapitalLoanConstants.loanStatusParamName) - .failWithCode("undo.discount.adjustment.only.allowed.for.active.loan"); + .failWithCode("undo.transaction.not.allowed.for.loan.status"); } throwExceptionIfValidationWarningsExist(dataValidationErrors); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapper.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapper.java new file mode 100644 index 00000000000..aaf5e315754 --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapper.java @@ -0,0 +1,134 @@ +/** + * 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.serialization.mapper; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanBreachSchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyScheduleTagDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDisbursementDetailDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroMapperConfig; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanChargeData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDisbursementDetailData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanRangeScheduleDelinquencyData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanSummaryData; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = AvroMapperConfig.class) +public interface WorkingCapitalLoanAccountDataMapper { + + int AVRO_DECIMAL_SCALE = 8; + + @Mapping(source = "delinquencyGraceDays", target = "graceOnArrearsAgeing") + @Mapping(source = "summary.overpayment", target = "totalOverpaid") + @Mapping(source = "dailyEir", target = "dailyEir", qualifiedByName = "toAvroDecimalScale") + @Mapping(source = "calculatedAnnualEir", target = "calculatedAnnualEir", qualifiedByName = "toAvroDecimalScale") + @Mapping(source = "numberOfRepayments", target = "actualNoTerm") + @Mapping(source = "delinquencyStartType", target = "delinquency.delinquencyStartType") + @Mapping(source = "delinquencyStartDate", target = "delinquency.delinquencyStartDate") + @Mapping(source = "breach.id", target = "breach.id") + @Mapping(source = "breach.name", target = "breach.name") + @Mapping(source = "breach.breachFrequency", target = "breach.breachFrequency") + @Mapping(source = "breach.breachFrequencyType", target = "breach.breachFrequencyType") + @Mapping(source = "breach.breachAmountCalculationType", target = "breach.breachAmountCalculationType") + @Mapping(source = "breach.breachAmount", target = "breach.breachAmount") + @Mapping(source = "breachGraceDays", target = "breach.breachGraceDays") + @Mapping(source = "breachStartDate", target = "breach.breachStartDate") + @Mapping(source = "nearBreach", target = "breach.nearBreach") + @Mapping(target = "breach.breachSchedule", ignore = true) + @Mapping(target = "overpaidOnDate", ignore = true) + @Mapping(target = "customData", ignore = true) + WorkingCapitalLoanAccountDataV1 map(WorkingCapitalLoanData source); + + @Mapping(source = "principal", target = "totalPrincipal") + @Mapping(source = "totalDisbursement", target = "principalDisbursed") + @Mapping(source = "fee", target = "feeChargesCharged") + @Mapping(source = "feePaid", target = "feeChargesPaid") + @Mapping(source = "feeOutstanding", target = "feeChargesOutstanding") + @Mapping(source = "penalty", target = "penaltyChargesCharged") + @Mapping(source = "penaltyPaid", target = "penaltyChargesPaid") + @Mapping(source = "penaltyOutstanding", target = "penaltyChargesOutstanding") + @Mapping(source = "principalAdjustment", target = "principalAdjustments") + @Mapping(target = "principalWrittenOff", ignore = true) + @Mapping(target = "feeChargesWrittenOff", ignore = true) + @Mapping(target = "penaltyChargesWrittenOff", ignore = true) + @Mapping(target = "totalWrittenOff", ignore = true) + @Mapping(target = "totalRecovered", ignore = true) + @Mapping(target = "writeoffReasonId", ignore = true) + @Mapping(target = "writeoffReason", ignore = true) + @Mapping(target = "chargeOffReasonId", ignore = true) + @Mapping(target = "chargeOffReason", ignore = true) + @Mapping(target = "totalPayoutRefund", ignore = true) + @Mapping(target = "totalPayoutRefundReversed", ignore = true) + @Mapping(target = "totalGoodwillCredit", ignore = true) + @Mapping(target = "totalGoodwillCreditReversed", ignore = true) + @Mapping(target = "totalChargeAdjustment", ignore = true) + @Mapping(target = "totalChargeAdjustmentReversed", ignore = true) + @Mapping(target = "totalCreditBalanceRefund", ignore = true) + @Mapping(target = "totalCreditBalanceRefundReversed", ignore = true) + @Mapping(target = "totalRepaymentTransaction", ignore = true) + @Mapping(target = "totalRepaymentTransactionReversed", ignore = true) + WorkingCapitalLoanSummaryDataV1 map(WorkingCapitalLoanSummaryData source); + + @Mapping(source = "delinquentPrincipal", target = "totalDelinquentAmount") + @Mapping(target = "lastPaymentDate", ignore = true) + @Mapping(target = "lastPaymentAmount", ignore = true) + @Mapping(target = "lastRepaymentDate", ignore = true) + @Mapping(target = "lastRepaymentAmount", ignore = true) + @Mapping(target = "delinquencySchedule", ignore = true) + WorkingCapitalLoanCollectionDataV1 map(WorkingCapitalLoanCollectionData source); + + @Mapping(target = "amountAccrued", ignore = true) + @Mapping(target = "amountUnrecognized", ignore = true) + @Mapping(target = "amountWrittenOff", ignore = true) + @Mapping(target = "customData", ignore = true) + WorkingCapitalLoanChargeDataV1 map(WorkingCapitalLoanChargeData source); + + @Mapping(target = "netDisbursalAmount", ignore = true) + WorkingCapitalLoanDisbursementDetailDataV1 map(WorkingCapitalLoanDisbursementDetailData source); + + @Mapping(target = "tags", ignore = true) + WorkingCapitalLoanDelinquencySchedulePeriodDataV1 map(WorkingCapitalLoanDelinquencyRangeScheduleData source); + + List mapDelinquencySchedule( + List source); + + WorkingCapitalLoanDelinquencyScheduleTagDataV1 map(WorkingCapitalLoanRangeScheduleDelinquencyData source); + + WorkingCapitalLoanBreachSchedulePeriodDataV1 map(WorkingCapitalLoanBreachScheduleData source); + + List mapBreachSchedule(List source); + + @Named("toAvroDecimalScale") + default BigDecimal toAvroDecimalScale(final BigDecimal value) { + return value == null ? null : value.setScale(AVRO_DECIMAL_SCALE, RoundingMode.HALF_UP); + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanBusinessEventSerializer.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanBusinessEventSerializer.java new file mode 100644 index 00000000000..18308f62bed --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanBusinessEventSerializer.java @@ -0,0 +1,219 @@ +/** + * 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.serialization.serializer; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.apache.avro.generic.GenericContainer; +import org.apache.fineract.avro.generator.ByteBufferSerializable; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalBreachDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyScheduleTagDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanBusinessEvent; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroDateTimeMapper; +import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.AbstractBusinessEventWithCustomDataSerializer; +import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer; +import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.ExternalEventCustomDataSerializer; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.workingcapitalloan.data.ChargeIdAndAmountHolder; +import org.apache.fineract.portfolio.workingcapitalloan.data.TransactionTypeTotalHolder; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransaction; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanTransactionRelationRepository; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanBreachScheduleMapper; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanDelinquencyRangeScheduleMapper; +import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanBreachScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanTransactionRepository; +import org.apache.fineract.portfolio.workingcapitalloan.serialization.mapper.WorkingCapitalLoanAccountDataMapper; +import org.apache.fineract.portfolio.workingcapitalloan.service.WorkingCapitalLoanApplicationReadPlatformService; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class WorkingCapitalLoanBusinessEventSerializer + extends AbstractBusinessEventWithCustomDataSerializer implements BusinessEventSerializer { + + private static final List SUMMARY_TOTAL_TRANSACTION_TYPES = List.of(LoanTransactionType.PAYOUT_REFUND, + LoanTransactionType.GOODWILL_CREDIT, LoanTransactionType.CHARGE_ADJUSTMENT, LoanTransactionType.CREDIT_BALANCE_REFUND, + LoanTransactionType.REPAYMENT); + + private final WorkingCapitalLoanApplicationReadPlatformService readPlatformService; + private final WorkingCapitalLoanAccountDataMapper mapper; + private final WorkingCapitalLoanTransactionRelationRepository transactionRelationRepository; + private final WorkingCapitalLoanTransactionRepository transactionRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleRepository rangeScheduleRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleMapper rangeScheduleMapper; + private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository tagHistoryRepository; + private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper tagHistoryMapper; + private final WorkingCapitalLoanBreachScheduleRepository breachScheduleRepository; + private final WorkingCapitalLoanBreachScheduleMapper breachScheduleMapper; + private final AvroDateTimeMapper avroDateTimeMapper; + private final List> externalEventCustomDataSerializers; + private final List chargeExternalEventCustomDataSerializers; + + @Override + public boolean canSerialize(BusinessEvent event) { + return event instanceof WorkingCapitalLoanBusinessEvent; + } + + @Override + public ByteBufferSerializable toAvroDTO(BusinessEvent rawEvent) { + final WorkingCapitalLoanBusinessEvent event = (WorkingCapitalLoanBusinessEvent) rawEvent; + final WorkingCapitalLoan loan = event.get(); + final WorkingCapitalLoanData data = readPlatformService.retrieveOne(loan.getId()); + final WorkingCapitalLoanAccountDataV1 result = mapper.map(data); + if (result.getDisbursementDetails() != null) { + result.getDisbursementDetails().forEach(detail -> detail.setNetDisbursalAmount(result.getNetDisbursalAmount())); + } + populateChargeAccruals(loan, result); + populateChargeCustomData(event, result); + populateSummaryTransactionTypeTotals(loan, result); + populateLastTransactions(loan, result); + populateDelinquencySchedule(loan, result); + populateBreachSchedule(loan, result); + result.setCustomData(collectCustomData(event)); + return result; + } + + private void populateChargeAccruals(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + if (result.getCharges() == null || result.getCharges().isEmpty() || loan.getLoanProduct() == null + || !loan.getLoanProduct().getAccountingRule().isAccrualWithDeferredRevenueAmortization()) { + return; + } + final Map accruedAmountsByChargeId = transactionRelationRepository + .fetchTransactionAmountPerCharge(loan.getId(), LoanTransactionType.ACCRUAL).stream() + .collect(Collectors.toMap(ChargeIdAndAmountHolder::chargeId, ChargeIdAndAmountHolder::amount)); + result.getCharges().forEach(charge -> { + final BigDecimal amountAccrued = accruedAmountsByChargeId.getOrDefault(charge.getId(), BigDecimal.ZERO); + charge.setAmountAccrued(amountAccrued); + charge.setAmountUnrecognized(MathUtil.subtractToZero(charge.getAmount(), amountAccrued)); + }); + } + + private void populateChargeCustomData(final WorkingCapitalLoanBusinessEvent event, final WorkingCapitalLoanAccountDataV1 result) { + if (result.getCharges() == null) { + return; + } + result.getCharges().forEach(charge -> charge.setCustomData(collectChargeCustomData(event, charge.getId()))); + } + + private Map collectChargeCustomData(final WorkingCapitalLoanBusinessEvent event, final Long chargeId) { + return chargeExternalEventCustomDataSerializers.stream().collect(HashMap::new, (map, serializer) -> { + final ByteBuffer buffer = serializer.serialize(event, chargeId); + if (buffer != null) { + map.put(serializer.key(), buffer); + } + }, HashMap::putAll); + } + + private void populateSummaryTransactionTypeTotals(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + final WorkingCapitalLoanSummaryDataV1 summary = result.getSummary(); + if (summary == null) { + return; + } + final Map> totals = transactionRepository + .fetchTotalsPerTypeAndReversed(loan.getId(), SUMMARY_TOTAL_TRANSACTION_TYPES).stream() + .collect(Collectors.groupingBy(TransactionTypeTotalHolder::transactionType, + Collectors.toMap(TransactionTypeTotalHolder::reversed, TransactionTypeTotalHolder::total))); + summary.setTotalPayoutRefund(totalOf(totals, LoanTransactionType.PAYOUT_REFUND, false)); + summary.setTotalPayoutRefundReversed(totalOf(totals, LoanTransactionType.PAYOUT_REFUND, true)); + summary.setTotalGoodwillCredit(totalOf(totals, LoanTransactionType.GOODWILL_CREDIT, false)); + summary.setTotalGoodwillCreditReversed(totalOf(totals, LoanTransactionType.GOODWILL_CREDIT, true)); + summary.setTotalChargeAdjustment(totalOf(totals, LoanTransactionType.CHARGE_ADJUSTMENT, false)); + summary.setTotalChargeAdjustmentReversed(totalOf(totals, LoanTransactionType.CHARGE_ADJUSTMENT, true)); + summary.setTotalCreditBalanceRefund(totalOf(totals, LoanTransactionType.CREDIT_BALANCE_REFUND, false)); + summary.setTotalCreditBalanceRefundReversed(totalOf(totals, LoanTransactionType.CREDIT_BALANCE_REFUND, true)); + summary.setTotalRepaymentTransaction(totalOf(totals, LoanTransactionType.REPAYMENT, false)); + summary.setTotalRepaymentTransactionReversed(totalOf(totals, LoanTransactionType.REPAYMENT, true)); + } + + private BigDecimal totalOf(final Map> totals, final LoanTransactionType transactionType, + final boolean reversed) { + return totals.getOrDefault(transactionType, Map.of()).getOrDefault(reversed, BigDecimal.ZERO); + } + + private void populateLastTransactions(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + final WorkingCapitalLoanCollectionDataV1 delinquent = result.getDelinquent(); + if (delinquent == null) { + return; + } + findLastActiveTransaction(loan, LoanTransactionType.getRepaymentLikeTransactionTypes()).ifPresent(transaction -> { + delinquent.setLastPaymentDate(avroDateTimeMapper.mapLocalDate(transaction.getTransactionDate())); + delinquent.setLastPaymentAmount(transaction.getTransactionAmount()); + }); + findLastActiveTransaction(loan, List.of(LoanTransactionType.REPAYMENT)).ifPresent(transaction -> { + delinquent.setLastRepaymentDate(avroDateTimeMapper.mapLocalDate(transaction.getTransactionDate())); + delinquent.setLastRepaymentAmount(transaction.getTransactionAmount()); + }); + } + + private Optional findLastActiveTransaction(final WorkingCapitalLoan loan, + final List transactionTypes) { + return transactionRepository.findActiveByTypesOrderByDateDesc(loan.getId(), transactionTypes).stream().findFirst(); + } + + private void populateDelinquencySchedule(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + final WorkingCapitalLoanCollectionDataV1 delinquent = result.getDelinquent(); + if (delinquent == null) { + return; + } + final List periods = mapper.mapDelinquencySchedule( + rangeScheduleMapper.toDataList(rangeScheduleRepository.findByLoanIdOrderByPeriodNumberAsc(loan.getId()))); + final Map> tagsByPeriod = tagHistoryRepository + .findByLoanIdAndLiftedOnDateIsNull(loan.getId()).stream() + .collect(Collectors.groupingBy(tag -> tag.getRangeSchedule().getPeriodNumber(), + Collectors.mapping(tag -> mapper.map(tagHistoryMapper.mapForCollectionData(tag)), Collectors.toList()))); + periods.forEach(period -> period.setTags(tagsByPeriod.get(period.getPeriodNumber()))); + delinquent.setDelinquencySchedule(periods); + } + + private void populateBreachSchedule(final WorkingCapitalLoan loan, final WorkingCapitalLoanAccountDataV1 result) { + final WorkingCapitalBreachDataV1 breach = result.getBreach(); + if (breach == null) { + return; + } + breach.setBreachSchedule(mapper.mapBreachSchedule( + breachScheduleMapper.toDataList(breachScheduleRepository.findByLoanIdOrderByPeriodNumberAsc(loan.getId())))); + } + + @Override + protected List> getExternalEventCustomDataSerializers() { + return externalEventCustomDataSerializers; + } + + @Override + public Class getSupportedSchema() { + return WorkingCapitalLoanAccountDataV1.class; + } +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanChargeExternalEventCustomDataSerializer.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanChargeExternalEventCustomDataSerializer.java new file mode 100644 index 00000000000..79251aff7de --- /dev/null +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/serializer/WorkingCapitalLoanChargeExternalEventCustomDataSerializer.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.serialization.serializer; + +import java.nio.ByteBuffer; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanBusinessEvent; + +public interface WorkingCapitalLoanChargeExternalEventCustomDataSerializer { + + ByteBuffer serialize(WorkingCapitalLoanBusinessEvent event, Long chargeId); + + String key(); +} diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java index 191d025b6b2..5e567229def 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanApplicationWritePlatformServiceImpl.java @@ -28,6 +28,9 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanApplicationModifiedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanCreatedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.service.LoanOriginatorLinkingService; import org.apache.fineract.portfolio.workingcapitalloan.WorkingCapitalLoanConstants; @@ -54,17 +57,20 @@ public class WorkingCapitalLoanApplicationWritePlatformServiceImpl implements Wo private final WorkingCapitalLoanNoteRepository noteRepository; private final ProjectedAmortizationLoanModelRepository projectedAmortizationLoanModelRepository; private final Optional loanOriginatorLinkingService; + private final BusinessEventNotifierService businessEventNotifierService; public WorkingCapitalLoanApplicationWritePlatformServiceImpl(WorkingCapitalLoanApplicationDataValidator validator, WorkingCapitalLoanRepository repository, WorkingCapitalLoanAssembler assembler, WorkingCapitalLoanNoteRepository noteRepository, ProjectedAmortizationLoanModelRepository projectedAmortizationLoanModelRepository, - @Qualifier("workingCapitalLoanOriginatorLinkingServiceImpl") Optional loanOriginatorLinkingService) { + @Qualifier("workingCapitalLoanOriginatorLinkingServiceImpl") Optional loanOriginatorLinkingService, + BusinessEventNotifierService businessEventNotifierService) { this.validator = validator; this.repository = repository; this.assembler = assembler; this.noteRepository = noteRepository; this.projectedAmortizationLoanModelRepository = projectedAmortizationLoanModelRepository; this.loanOriginatorLinkingService = loanOriginatorLinkingService; + this.businessEventNotifierService = businessEventNotifierService; } @Transactional @@ -80,6 +86,8 @@ public CommandProcessingResult submitApplication(final JsonCommand command) { createNote(submittedOnNote, saved); attachOriginatorsIfProvided(command, saved); + this.businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanCreatedBusinessEvent(saved)); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(saved.getId()) // @@ -113,6 +121,8 @@ public CommandProcessingResult modifyApplication(final Long loanId, final JsonCo final String submittedOnNote = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.submittedOnNoteParameterName); createNote(submittedOnNote, saved); + this.businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanApplicationModifiedBusinessEvent(saved)); + return new CommandProcessingResultBuilder() // .withEntityId(loanId) // .withEntityExternalId(saved.getExternalId()) // diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java index 5ae60f84db8..c45551a5694 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanChargeWritePlatformServiceImpl.java @@ -37,6 +37,8 @@ import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanBalanceChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanStatusChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanChargeAdjustmentPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanChargeAdjustmentPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; @@ -143,6 +145,9 @@ public CommandProcessingResult createLoanCharge(Long loanId, JsonCommand command chargeAccrualService.processOnChargeAdded(loan, loanCharge); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, statusBeforeCharge); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanCharge.getId()) // @@ -274,6 +279,7 @@ public CommandProcessingResult adjustmentForLoanCharge(final Long loanId, final adjustmentTx.getLoanTransactionRelations().add(relation); transactionRepository.saveAndFlush(adjustmentTx); + final LoanStatus oldStatus = loan.getLoanStatus(); final WorkingCapitalLoanTransactionAllocation allocation = transactionProcessor.processRepaymentLikeTransaction(loan, adjustmentTx, LoanTransactionType.CHARGE_ADJUSTMENT, transactionDate, amount); @@ -290,6 +296,10 @@ public CommandProcessingResult adjustmentForLoanCharge(final Long loanId, final businessEventNotifierService .notifyPostBusinessEvent(new WorkingCapitalLoanChargeAdjustmentPostBusinessEvent(adjustmentTx, loan.getId())); + workingCapitalLoanRepository.saveAndFlush(loan); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(wcLoanChargeId) // @@ -376,4 +386,14 @@ private void addChargeToBalance(final WorkingCapitalLoanBalance balance, final W balance.setFee(balance.getFee().add(loanCharge.getAmount())); } } + + private void notifyStatusChanged(final WorkingCapitalLoan loan, final LoanStatus oldStatus) { + if (oldStatus != loan.getLoanStatus()) { + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanStatusChangedBusinessEvent(loan)); + } + } + + private void notifyBalanceChanged(final WorkingCapitalLoan loan) { + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanBalanceChangedBusinessEvent(loan)); + } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java index 518294ca764..70bf17cf009 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationService.java @@ -30,7 +30,7 @@ public interface WorkingCapitalLoanDelinquencyClassificationService { void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate); - void applyDelinquencyTagForRange(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyRangeSchedule range, + boolean applyDelinquencyTagForRange(WorkingCapitalLoan loan, WorkingCapitalLoanDelinquencyRangeSchedule range, DelinquencyRange currentRange, LocalDate businessDate); boolean isDelinquencyDisabled(WorkingCapitalLoan loan, LocalDate date); diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java index c61ec769c22..245e6b6923c 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyClassificationServiceImpl.java @@ -31,6 +31,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoan; @@ -50,6 +52,7 @@ public class WorkingCapitalLoanDelinquencyClassificationServiceImpl implements W private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository delinquencyRangeScheduleTagHistoryRepository; private final WorkingCapitalLoanDelinquencyActionRepository delinquencyActionRepository; private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository; + private final BusinessEventNotifierService businessEventNotifierService; /** * If ENABLE_INSTANT_DELINQUENCY_CALCULATION is set to true in global config, classifies the delinquency of a loan @@ -90,6 +93,7 @@ public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate) List delinquencyRangeScheduleList = delinquencyRangeScheduleRepository .findByLoanIdOrderByPeriodNumberAsc(loan.getId()); + boolean delinquencyRangeChanged = false; for (WorkingCapitalLoanDelinquencyRangeSchedule range : delinquencyRangeScheduleList) { if (!Objects.equals(range.getReset(), true) && range.getToDate().isBefore(businessDate)) { long rangeDelinquentDays = range.getOutstandingAmount().compareTo(BigDecimal.ZERO) > 0 @@ -102,14 +106,19 @@ public void classifyDelinquency(WorkingCapitalLoan loan, LocalDate businessDate) range.setDelinquentDays(rangeDelinquentDays); Optional delinquencyRangeByDays = findDelinquencyRangeByDays( loan.getLoanProductRelatedDetails().getDelinquencyBucket(), (int) rangeDelinquentDays); - applyDelinquencyTagForRange(loan, range, delinquencyRangeByDays.orElse(null), businessDate); + delinquencyRangeChanged |= applyDelinquencyTagForRange(loan, range, delinquencyRangeByDays.orElse(null), businessDate); } else { range.setDelinquentAmount(BigDecimal.ZERO); range.setDelinquentDays(0L); - applyDelinquencyTagForRange(loan, range, null, businessDate); + delinquencyRangeChanged |= applyDelinquencyTagForRange(loan, range, null, businessDate); } } } + + if (delinquencyRangeChanged) { + delinquencyRangeScheduleTagHistoryRepository.flush(); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent(loan)); + } } /** @@ -169,9 +178,10 @@ private Optional findDelinquencyRangeByDays(final DelinquencyB * the current delinquency range to be applied; can be null to lift all previous tags * @param businessDate * the date on which the tagging operation is performed + * @return true if the tag history was changed (a tag added or lifted), false if nothing changed */ @Override - public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final WorkingCapitalLoanDelinquencyRangeSchedule range, + public boolean applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final WorkingCapitalLoanDelinquencyRangeSchedule range, final DelinquencyRange currentRange, final LocalDate businessDate) { List updatedList = new ArrayList<>(); List rangeScheduleTagHistoryList = delinquencyRangeScheduleTagHistoryRepository @@ -183,7 +193,7 @@ public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final Wor // do nothing if currentRange is in rangeScheduleTagHistoryList or last and currentRange are null if ((last == null && currentRange == null) || (last != null && currentRange != null && rangeScheduleTagHistoryList.stream() .anyMatch(tag -> Objects.equals(tag.getDelinquencyRange().getId(), currentRange.getId())))) { - return; + return false; } if (currentRange == null) { @@ -202,6 +212,7 @@ public void applyDelinquencyTagForRange(final WorkingCapitalLoan loan, final Wor } delinquencyRangeScheduleTagHistoryRepository.saveAll(updatedList); + return !updatedList.isEmpty(); } } diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java index b54f4e66280..d646604fab8 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanDelinquencyReadPlatformServiceImpl.java @@ -19,6 +19,9 @@ package org.apache.fineract.portfolio.workingcapitalloan.service; +import static org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyPauseUtils.isPauseActiveOnDate; +import static org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyPauseUtils.resolveEffectivePauseEnd; + import java.math.BigDecimal; import java.time.LocalDate; import java.util.Comparator; @@ -27,12 +30,16 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; +import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyTagHistoryData; import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanRangeScheduleDelinquencyData; +import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyAction; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeSchedule; import org.apache.fineract.portfolio.workingcapitalloan.domain.WorkingCapitalLoanDelinquencyRangeScheduleTagHistory; import org.apache.fineract.portfolio.workingcapitalloan.mapper.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper; +import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyActionRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleRepository; import org.apache.fineract.portfolio.workingcapitalloan.repository.WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository; import org.springframework.stereotype.Service; @@ -47,6 +54,7 @@ public class WorkingCapitalLoanDelinquencyReadPlatformServiceImpl implements Wor private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryMapper delinquencyRangeScheduleTagHistoryMapper; private final WorkingCapitalLoanDelinquencyRangeScheduleTagHistoryRepository delinquencyRangeScheduleTagHistoryRepository; private final WorkingCapitalLoanDelinquencyRangeScheduleRepository delinquencyRangeScheduleRepository; + private final WorkingCapitalLoanDelinquencyActionRepository delinquencyActionRepository; @Override public WorkingCapitalLoanCollectionData getCollectionData(Long loanId, LocalDate businessDate) { @@ -73,10 +81,26 @@ public WorkingCapitalLoanCollectionData getCollectionData(Long loanId, LocalDate .map(WorkingCapitalLoanDelinquencyRangeSchedule::getDelinquentDays).ifPresent(template::setPastDueDays); template.setInstallmentLevelDelinquency(list); + template.setDelinquencyPausePeriods(retrieveDelinquencyPausePeriods(loanId, businessDate)); return template; } + private List retrieveDelinquencyPausePeriods(final Long loanId, final LocalDate businessDate) { + final List pauses = delinquencyActionRepository + .findByWorkingCapitalLoanIdAndActionOrderByStartDateAsc(loanId, DelinquencyAction.PAUSE); + final List resumes = delinquencyActionRepository + .findByWorkingCapitalLoanIdAndActionOrderByStartDateAsc(loanId, DelinquencyAction.RESUME); + return pauses.stream().map(pause -> toPausePeriod(pause, resumes, businessDate)).toList(); + } + + private DelinquencyPausePeriod toPausePeriod(final WorkingCapitalLoanDelinquencyAction pause, + final List resumes, final LocalDate businessDate) { + final LocalDate effectiveEnd = resolveEffectivePauseEnd(pause, resumes); + return new DelinquencyPausePeriod(isPauseActiveOnDate(pause.getStartDate(), effectiveEnd, businessDate), pause.getStartDate(), + effectiveEnd); + } + @Override public List retrieveDelinquencyRangeScheduleTagHistory(Long loanId) { List byLoanIdOrderByAddedOnDateDesc = delinquencyRangeScheduleTagHistoryRepository diff --git a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java index 60c714ea618..281898cac35 100644 --- a/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java +++ b/fineract-working-capital-loan/src/main/java/org/apache/fineract/portfolio/workingcapitalloan/service/WorkingCapitalLoanWritePlatformServiceImpl.java @@ -44,6 +44,13 @@ import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanApprovedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanBalanceChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanDisbursalBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanRejectedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanStatusChangedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanUndoApprovalBusinessEvent; +import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.loan.WorkingCapitalLoanUndoDisbursalBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDisbursalTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.workingcapitalloan.transaction.WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent; @@ -123,6 +130,7 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC final AppUser currentUser = this.context.authenticatedUser(); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_APPROVED, loan); // Approved date @@ -169,6 +177,9 @@ public CommandProcessingResult approveApplication(final Long loanId, final JsonC createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName), loan); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanApprovedBusinessEvent(loan)); + notifyStatusChanged(loan, oldStatus); + final Map changes = new LinkedHashMap<>(); changes.put(WorkingCapitalLoanConstants.approvedOnDateParamName, approvedOnDate); changes.put("status", loan.getLoanStatus()); @@ -193,6 +204,7 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final this.validator.validateUndoApproval(command.json()); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_APPROVAL_UNDO, loan); loan.setApprovedOnDate(null); @@ -209,6 +221,9 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName), loan); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanUndoApprovalBusinessEvent(loan)); + notifyStatusChanged(loan, oldStatus); + final Map changes = new LinkedHashMap<>(); changes.put("status", loan.getLoanStatus()); @@ -234,6 +249,7 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo final AppUser currentUser = this.context.authenticatedUser(); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_REJECTED, loan); final LocalDate rejectedOnDate = command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.rejectedOnDateParamName); @@ -244,6 +260,9 @@ public CommandProcessingResult rejectApplication(final Long loanId, final JsonCo createNote(command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName), loan); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanRejectedBusinessEvent(loan)); + notifyStatusChanged(loan, oldStatus); + final Map changes = new LinkedHashMap<>(); changes.put(WorkingCapitalLoanConstants.rejectedOnDateParamName, rejectedOnDate); changes.put("status", loan.getLoanStatus()); @@ -293,6 +312,7 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand changes.put(WorkingCapitalLoanConstants.classificationIdParamName, classificationId); final PaymentDetail paymentDetail = createAndPersistPaymentDetailFromCommand(command, changes); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_DISBURSED, loan); if (!loan.getDisbursementDetails().isEmpty()) { @@ -347,6 +367,10 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand changes.put("status", loan.getLoanStatus()); handleNote(loan, command, changes); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanDisbursalBusinessEvent(loan)); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + log.debug("Working capital loan {} disbursed by user {}", loanId, currentUser != null ? currentUser.getId() : "system"); return new CommandProcessingResultBuilder() // @@ -375,6 +399,7 @@ public CommandProcessingResult undoDisbursal(final Long loanId, final JsonComman ensureUndoDisbursalAllowed(loan); + final LoanStatus oldStatus = loan.getLoanStatus(); this.stateMachine.transition(WorkingCapitalLoanEvent.LOAN_DISBURSAL_UNDO, loan); final WorkingCapitalLoanTransaction reversedTransaction = reverseDisbursementTransactionAndResetBalance(loan); @@ -401,6 +426,10 @@ public CommandProcessingResult undoDisbursal(final Long loanId, final JsonComman changes.put("actualAmount", null); handleNote(loan, command, changes); + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanUndoDisbursalBusinessEvent(loan)); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + log.debug("Working capital loan {} disbursal undone", loanId); return new CommandProcessingResultBuilder() // @@ -515,6 +544,8 @@ public CommandProcessingResult makeDiscountFee(Long loanId, JsonCommand command) delinquencyRangeScheduleService.reprocessDelinquencySchedule(loan); loanRepository.saveAndFlush(loan); + notifyBalanceChanged(loan); + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(discountTransaction.getId()) .withEntityExternalId(discountTransaction.getExternalId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()) .withLoanId(loanId).with(changes).build(); @@ -586,13 +617,15 @@ public CommandProcessingResult makeDiscountFeeAdjustment(final Long loanId, fina // The principal change moves the remaining-balance cap, so the delinquency schedule must be re-derived. delinquencyRangeScheduleService.reprocessDelinquencySchedule(loan); + final LoanStatus oldStatus = loan.getLoanStatus(); + stateMachine.determineAndTransition(loan, transactionDate); transactionProcessor.triggerInlineAmortizationIfLoanClosed(loan, transactionDate); // A discount-fee adjustment can pay down principal and close the loan, so accrue any pending charge income. chargeAccrualService.accrueOnClosure(loan, transactionDate); changes.put("status", loan.getLoanStatus()); - loanRepository.save(loan); + loanRepository.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); createNote(noteText, loan); @@ -606,6 +639,8 @@ public CommandProcessingResult makeDiscountFeeAdjustment(final Long loanId, fina } businessEventNotifierService .notifyPostBusinessEvent(new WorkingCapitalLoanDiscountFeeAdjustmentTransactionBusinessEvent(adjustmentTransaction)); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(adjustmentTransaction.getId()) .withEntityExternalId(adjustmentTransaction.getExternalId()).withSubEntityId(relatedDiscountTransaction.getId()) .withSubEntityExternalId(relatedDiscountTransaction.getExternalId()).withOfficeId(loan.getOfficeId()) @@ -642,12 +677,23 @@ private CommandProcessingResult undoDiscountFeeAdjustment(final WorkingCapitalLo updateBalanceForDiscountChange(loan, adjustmentTransaction.getTransactionAmount().negate(), true); // The principal change moves the remaining-balance cap, so the delinquency schedule must be re-derived. delinquencyRangeScheduleService.reprocessDelinquencySchedule(loan); + + // Restoring the outstanding can reopen a loan the adjustment had closed, so re-run the status transition. + final LocalDate reversedOnDate = adjustmentTransaction.getReversedOnDate(); + final LoanStatus oldStatus = loan.getLoanStatus(); + + stateMachine.determineAndTransition(loan, reversedOnDate); + loanRepository.saveAndFlush(loan); final String noteText = command.stringValueOfParameterNamed(WorkingCapitalLoanConstants.noteParamName); createNote(noteText, loan); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + final Map changes = new LinkedHashMap<>(); + changes.put("status", loan.getLoanStatus()); if (StringUtils.isNotBlank(noteText)) { changes.put(WorkingCapitalLoanConstants.noteParamName, noteText); } @@ -667,6 +713,7 @@ private CommandProcessingResult makeRepaymentLikeTransaction(final Long loanId, .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); this.validator.validateRepayment(command.json(), loan, transactionType); + final LoanStatus oldStatus = loan.getLoanStatus(); final LocalDate transactionDate = command.localDateValueOfParameterNamed(WorkingCapitalLoanConstants.transactionDateParamName); final BigDecimal transactionAmount = this.fromApiJsonHelper .extractBigDecimalNamed(WorkingCapitalLoanConstants.transactionAmountParamName, command.parsedJson(), new HashSet<>()); @@ -700,6 +747,10 @@ private CommandProcessingResult makeRepaymentLikeTransaction(final Long loanId, notifyPostBusinessEvent(transactionType, transaction, loan); + this.loanRepository.saveAndFlush(loan); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(transaction.getId()) // @@ -722,6 +773,16 @@ private void notifyPostBusinessEvent(LoanTransactionType transactionType, Workin } } + private void notifyStatusChanged(final WorkingCapitalLoan loan, final LoanStatus oldStatus) { + if (oldStatus != loan.getLoanStatus()) { + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanStatusChangedBusinessEvent(loan)); + } + } + + private void notifyBalanceChanged(final WorkingCapitalLoan loan) { + businessEventNotifierService.notifyPostBusinessEvent(new WorkingCapitalLoanBalanceChangedBusinessEvent(loan)); + } + private WorkingCapitalLoanTransaction resolveNewTransaction(final LoanTransactionType transactionType, WorkingCapitalLoan loan, BigDecimal transactionAmount, PaymentDetail paymentDetail, LocalDate transactionDate, CodeValue classification, ExternalId txnExternalId) { @@ -750,6 +811,7 @@ public CommandProcessingResult creditBalanceRefund(final Long loanId, final Json .orElseThrow(() -> new WorkingCapitalLoanNotFoundException(loanId)); this.validator.validateCreditBalanceRefund(command.json(), loan); + final LoanStatus oldStatus = loan.getLoanStatus(); if (loan.getLoanStatus() != LoanStatus.OVERPAID) { throw new PlatformApiDataValidationException("validation.msg.wc.loan.transition.not.allowed", "Credit balance refund is allowed only for overpaid loans", "loanStatus"); @@ -824,6 +886,9 @@ public CommandProcessingResult creditBalanceRefund(final Long loanId, final Json businessEventNotifierService.notifyPostBusinessEvent( new WorkingCapitalLoanCreditBalanceRefundTransactionBusinessEvent(creditBalanceRefundTransaction, loan.getId())); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(creditBalanceRefundTransaction.getId()) // @@ -887,6 +952,7 @@ public CommandProcessingResult undoTransaction(final WorkingCapitalLoan loan, fi validator.validateUndoTransaction(command, loan, transaction); + final LoanStatus oldStatus = loan.getLoanStatus(); Map changes = new HashMap<>(); changes.put("reversed", true); transaction.setReversed(true); @@ -932,6 +998,10 @@ public CommandProcessingResult undoTransaction(final WorkingCapitalLoan loan, fi handleNote(loan, command, changes); + this.loanRepository.saveAndFlush(loan); + notifyBalanceChanged(loan); + notifyStatusChanged(loan, oldStatus); + return new CommandProcessingResultBuilder().withLoanId(loan.getId()).withLoanExternalId(loan.getExternalId()) .withEntityId(transaction.getId()).withEntityExternalId(transaction.getExternalId()).with(changes).build(); } 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 349839a541c..e7203dbe33b 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 @@ -83,4 +83,5 @@ + diff --git a/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0062_wc_loan_details_external_events.xml b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0062_wc_loan_details_external_events.xml new file mode 100644 index 00000000000..49c337462f9 --- /dev/null +++ b/fineract-working-capital-loan/src/main/resources/db/changelog/tenant/module/workingcapitalloan/parts/0062_wc_loan_details_external_events.xml @@ -0,0 +1,126 @@ + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanCreatedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanApplicationModifiedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanApprovedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanUndoApprovalBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanRejectedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanDisbursalBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanUndoDisbursalBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanStatusChangedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanBalanceChangedBusinessEvent' + + + + + + + + + + SELECT COUNT(*) FROM m_external_event_configuration WHERE type = 'WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent' + + + + + + + + diff --git a/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapperTest.java b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapperTest.java new file mode 100644 index 00000000000..1244be9d6aa --- /dev/null +++ b/fineract-working-capital-loan/src/test/java/org/apache/fineract/portfolio/workingcapitalloan/serialization/mapper/WorkingCapitalLoanAccountDataMapperTest.java @@ -0,0 +1,611 @@ +/** + * 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.serialization.mapper; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; +import org.apache.fineract.avro.generic.v1.CurrencyDataV1; +import org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1; +import org.apache.fineract.avro.loan.v1.LoanApplicationTimelineDataV1; +import org.apache.fineract.avro.loan.v1.LoanStatusEnumDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalBreachDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanAccountDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanBreachSchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanChargeDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanCollectionDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencyDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDelinquencySchedulePeriodDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanDisbursementDetailDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalLoanSummaryDataV1; +import org.apache.fineract.avro.workingcapitalloan.v1.WorkingCapitalNearBreachDataV1; +import org.apache.fineract.infrastructure.codes.data.CodeValueData; +import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.AvroDateTimeMapper; +import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.support.ExternalIdMapper; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.loanaccount.data.DelinquencyPausePeriod; +import org.apache.fineract.portfolio.loanaccount.data.LoanApplicationTimelineData; +import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData; +import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanBreachScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanChargeData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanCollectionData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDelinquencyRangeScheduleData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanDisbursementDetailData; +import org.apache.fineract.portfolio.workingcapitalloan.data.WorkingCapitalLoanSummaryData; +import org.apache.fineract.portfolio.workingcapitalloanbreach.data.WorkingCapitalBreachData; +import org.apache.fineract.portfolio.workingcapitalloannearbreach.data.WorkingCapitalNearBreachData; +import org.junit.jupiter.api.Test; + +class WorkingCapitalLoanAccountDataMapperTest { + + private final WorkingCapitalLoanAccountDataMapper mapper = new WorkingCapitalLoanAccountDataMapperImpl(new AvroDateTimeMapper(), + new ExternalIdMapper()); + + @Test + void map_nullSource_returnsNull() { + assertNull(mapper.map((WorkingCapitalLoanData) null)); + } + + @Test + void map_loanData_coversAllTopLevelFields() { + final WorkingCapitalLoanData source = fullLoanData(); + + final WorkingCapitalLoanAccountDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(101L, result.getId()); + assertEquals("WC-000101", result.getAccountNo()); + assertEquals("ext-101", result.getExternalId()); + assertEquals(11L, result.getClientId()); + assertEquals("CL-11", result.getClientAccountNo()); + assertEquals("John Doe", result.getClientName()); + assertEquals(22L, result.getClientOfficeId()); + assertEquals("client-ext", result.getClientExternalId()); + assertEquals(33L, result.getLoanProductId()); + assertEquals("WC Product", result.getLoanProductName()); + assertEquals("WC product description", result.getLoanProductDescription()); + assertEquals(44L, result.getFundId()); + assertEquals("Main Fund", result.getFundName()); + assertEquals(new BigDecimal("1000.00"), result.getPrincipal()); + assertEquals(new BigDecimal("1100.00"), result.getApprovedPrincipal()); + assertEquals(new BigDecimal("1200.00"), result.getProposedPrincipal()); + assertEquals(new BigDecimal("980.00"), result.getNetDisbursalAmount()); + assertEquals(90, result.getNumberOfRepayments()); + assertEquals(30, result.getRepaymentEvery()); + assertEquals(7, result.getLoanProductCounter()); + assertEquals(360, result.getNpvDayCount()); + assertEquals(new BigDecimal("2.50"), result.getPaymentRate()); + assertEquals(new BigDecimal("12.00"), result.getDiscountFee()); + assertEquals(new BigDecimal("13.00"), result.getProposedDiscountFee()); + assertEquals(new BigDecimal("14.00"), result.getApprovedDiscountFee()); + assertEquals(new BigDecimal("120.00"), result.getPeriodPaymentAmount()); + assertEquals(new BigDecimal("5000.00"), result.getTotalPaymentVolume()); + assertEquals(Boolean.TRUE, result.getChargedOff()); + assertEquals(Boolean.TRUE, result.getEnableInstallmentLevelDelinquency()); + + // actualNoTerm duplicates numberOfRepayments + assertEquals(90, result.getActualNoTerm()); + + // delinquencyGraceDays is exposed under the legacy loan field name + assertEquals(7, result.getGraceOnArrearsAgeing()); + + // totalOverpaid is pulled from the nested summary.overpayment + assertEquals(new BigDecimal("9.99"), result.getTotalOverpaid()); + + // EIR values are re-scaled to the fixed avro decimal scale (8, HALF_UP) + assertEquals(new BigDecimal("0.05000000"), result.getDailyEir()); + assertEquals(8, result.getDailyEir().scale()); + assertEquals(new BigDecimal("18.25000000"), result.getCalculatedAnnualEir()); + assertEquals(8, result.getCalculatedAnnualEir().scale()); + assertEquals("2024-02-01", result.getLastClosedBusinessDate()); + assertStringEnum(source.getAmortizationType(), result.getAmortizationType()); + assertStringEnum(source.getRepaymentFrequencyType(), result.getRepaymentFrequencyType()); + assertNotNull(result.getStatus()); + assertNotNull(result.getTimeline()); + assertNotNull(result.getSummary()); + assertNotNull(result.getDelinquent()); + assertNotNull(result.getDelinquency()); + assertNotNull(result.getBreach()); + assertEquals(1, result.getCharges().size()); + assertEquals(1, result.getDisbursementDetails().size()); + assertEquals(1, result.getOriginators().size()); + + // serializer-only fields stay unmapped + assertNull(result.getOverpaidOnDate()); + assertNull(result.getCustomData()); + } + + @Test + void map_loanData_mapsStatusFully() { + final LoanStatusEnumDataV1 status = mapper.map(fullLoanData()).getStatus(); + + assertNotNull(status); + assertEquals(300, status.getId()); + assertEquals("loanStatusType.active", status.getCode()); + assertEquals("Active", status.getValue()); + assertEquals(Boolean.FALSE, status.getPendingApproval()); + assertEquals(Boolean.FALSE, status.getWaitingForDisbursal()); + assertEquals(Boolean.TRUE, status.getActive()); + assertEquals(Boolean.FALSE, status.getClosedObligationsMet()); + assertEquals(Boolean.FALSE, status.getClosedWrittenOff()); + assertEquals(Boolean.FALSE, status.getClosedRescheduled()); + assertEquals(Boolean.FALSE, status.getClosed()); + assertEquals(Boolean.FALSE, status.getOverpaid()); + } + + @Test + void map_loanData_mapsTimelineFully() { + final LoanApplicationTimelineDataV1 timeline = mapper.map(fullLoanData()).getTimeline(); + + assertNotNull(timeline); + assertEquals("2024-01-01", timeline.getSubmittedOnDate()); + assertEquals("sub-user", timeline.getSubmittedByUsername()); + assertEquals("sub-first", timeline.getSubmittedByFirstname()); + assertEquals("sub-last", timeline.getSubmittedByLastname()); + assertEquals("2024-01-02", timeline.getRejectedOnDate()); + assertEquals("rej-user", timeline.getRejectedByUsername()); + assertEquals("rej-first", timeline.getRejectedByFirstname()); + assertEquals("rej-last", timeline.getRejectedByLastname()); + assertEquals("2024-01-03", timeline.getWithdrawnOnDate()); + assertEquals("wd-user", timeline.getWithdrawnByUsername()); + assertEquals("wd-first", timeline.getWithdrawnByFirstname()); + assertEquals("wd-last", timeline.getWithdrawnByLastname()); + assertEquals("2024-01-04", timeline.getApprovedOnDate()); + assertEquals("app-user", timeline.getApprovedByUsername()); + assertEquals("app-first", timeline.getApprovedByFirstname()); + assertEquals("app-last", timeline.getApprovedByLastname()); + assertEquals("2024-01-05", timeline.getExpectedDisbursementDate()); + assertEquals("2024-01-06", timeline.getActualDisbursementDate()); + assertEquals("dis-user", timeline.getDisbursedByUsername()); + assertEquals("dis-first", timeline.getDisbursedByFirstname()); + assertEquals("dis-last", timeline.getDisbursedByLastname()); + assertEquals("2024-01-07", timeline.getClosedOnDate()); + assertEquals("cl-user", timeline.getClosedByUsername()); + assertEquals("cl-first", timeline.getClosedByFirstname()); + assertEquals("cl-last", timeline.getClosedByLastname()); + assertEquals("2024-01-08", timeline.getExpectedMaturityDate()); + assertEquals("2024-01-09", timeline.getWriteOffOnDate()); + assertEquals("wo-user", timeline.getWriteOffByUsername()); + assertEquals("wo-first", timeline.getWriteOffByFirstname()); + assertEquals("wo-last", timeline.getWriteOffByLastname()); + assertEquals("2024-01-10", timeline.getChargedOffOnDate()); + assertEquals("co-user", timeline.getChargedOffByUsername()); + assertEquals("co-first", timeline.getChargedOffByFirstname()); + assertEquals("co-last", timeline.getChargedOffByLastname()); + } + + @Test + void map_loanData_mapsOriginatorsFully() { + final var originator = mapper.map(fullLoanData()).getOriginators().getFirst(); + + assertNotNull(originator); + assertEquals(70L, originator.getId()); + assertEquals("orig-ext", originator.getExternalId()); + assertEquals("Origin Bank", originator.getName()); + assertEquals("ACTIVE", originator.getStatus()); + + assertNotNull(originator.getOriginatorType()); + assertEquals(71L, originator.getOriginatorType().getId()); + assertEquals("Broker", originator.getOriginatorType().getName()); + assertEquals(1, originator.getOriginatorType().getPosition()); + assertEquals("originator type", originator.getOriginatorType().getDescription()); + assertEquals(Boolean.TRUE, originator.getOriginatorType().getActive()); + assertEquals(Boolean.FALSE, originator.getOriginatorType().getMandatory()); + + assertNotNull(originator.getChannelType()); + assertEquals(72L, originator.getChannelType().getId()); + assertEquals("Online", originator.getChannelType().getName()); + } + + @Test + void map_loanData_mapsDelinquencyFully() { + final WorkingCapitalLoanData source = fullLoanData(); + + final WorkingCapitalLoanDelinquencyDataV1 delinquency = mapper.map(source).getDelinquency(); + + assertNotNull(delinquency); + assertStringEnum(source.getDelinquencyStartType(), delinquency.getDelinquencyStartType()); + assertEquals("2024-01-05", delinquency.getDelinquencyStartDate()); + } + + @Test + void map_loanData_mapsBreachWithNearBreachFully() { + final WorkingCapitalBreachDataV1 breach = mapper.map(fullLoanData()).getBreach(); + + assertNotNull(breach); + assertEquals(80L, breach.getId()); + assertEquals("Breach rule", breach.getName()); + assertEquals(2, breach.getBreachFrequency()); + assertStringEnum(fullBreach().getBreachFrequencyType(), breach.getBreachFrequencyType()); + assertStringEnum(fullBreach().getBreachAmountCalculationType(), breach.getBreachAmountCalculationType()); + assertEquals(new BigDecimal("500.00"), breach.getBreachAmount()); + + // loan-level fields lifted into the breach record + assertEquals(3, breach.getBreachGraceDays()); + assertEquals("2024-01-06", breach.getBreachStartDate()); + + final WorkingCapitalNearBreachDataV1 nearBreach = breach.getNearBreach(); + assertNotNull(nearBreach); + assertEquals(81L, nearBreach.getId()); + assertEquals("Near breach rule", nearBreach.getName()); + assertEquals(1, nearBreach.getFrequency()); + assertStringEnum(fullNearBreach().getFrequencyType(), nearBreach.getFrequencyType()); + assertEquals(new BigDecimal("450.00"), nearBreach.getThreshold()); + + // serializer-only field stays unmapped + assertNull(breach.getBreachSchedule()); + } + + @Test + void map_loanData_withoutBreachConfig_stillCreatesBreachRecordFromLoanFields() { + final WorkingCapitalLoanData source = fullLoanData(); + source.setBreach(null); + source.setNearBreach(null); + + final WorkingCapitalBreachDataV1 breach = mapper.map(source).getBreach(); + + assertNotNull(breach); + assertNull(breach.getId()); + assertNull(breach.getName()); + assertNull(breach.getNearBreach()); + assertEquals(3, breach.getBreachGraceDays()); + assertEquals("2024-01-06", breach.getBreachStartDate()); + } + + @Test + void map_summary_coversAllFields() { + final WorkingCapitalLoanSummaryData source = fullSummary(); + + final WorkingCapitalLoanSummaryDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(new BigDecimal("1000"), result.getTotalPrincipal()); + assertEquals(new BigDecimal("950"), result.getPrincipalDisbursed()); + assertEquals(new BigDecimal("50"), result.getFeeChargesCharged()); + assertEquals(new BigDecimal("20"), result.getFeeChargesPaid()); + assertEquals(new BigDecimal("30"), result.getFeeChargesOutstanding()); + assertEquals(new BigDecimal("10"), result.getPenaltyChargesCharged()); + assertEquals(new BigDecimal("4"), result.getPenaltyChargesPaid()); + assertEquals(new BigDecimal("6"), result.getPenaltyChargesOutstanding()); + assertEquals(new BigDecimal("100"), result.getPrincipalPaid()); + assertEquals(new BigDecimal("900"), result.getPrincipalOutstanding()); + assertEquals(new BigDecimal("1500"), result.getTotalExpectedRepayment()); + assertEquals(new BigDecimal("600"), result.getTotalRepayment()); + assertEquals(new BigDecimal("900"), result.getTotalOutstanding()); + assertEquals(new BigDecimal("15"), result.getRealizedIncomeFromDiscountFee()); + assertEquals(new BigDecimal("35"), result.getUnrealizedIncomeFromDiscountFee()); + assertEquals(new BigDecimal("50"), result.getTotalDiscountFee()); + assertEquals(new BigDecimal("5"), result.getTotalDiscountFeeAdjustment()); + assertCurrency(source.getCurrency(), result.getCurrency()); + } + + @Test + void map_summary_leavesStubAndSerializerFilledFieldsNull() { + final WorkingCapitalLoanSummaryDataV1 result = mapper.map(fullSummary()); + + // not-yet-implemented functionality stubs + assertNull(result.getPrincipalAdjustments()); + assertNull(result.getPrincipalWrittenOff()); + assertNull(result.getFeeChargesWrittenOff()); + assertNull(result.getPenaltyChargesWrittenOff()); + assertNull(result.getTotalWrittenOff()); + assertNull(result.getTotalRecovered()); + assertNull(result.getWriteoffReasonId()); + assertNull(result.getWriteoffReason()); + assertNull(result.getChargeOffReasonId()); + assertNull(result.getChargeOffReason()); + + // per-transaction-type totals are filled by the serializer, not the mapper + assertNull(result.getTotalPayoutRefund()); + assertNull(result.getTotalPayoutRefundReversed()); + assertNull(result.getTotalGoodwillCredit()); + assertNull(result.getTotalGoodwillCreditReversed()); + assertNull(result.getTotalChargeAdjustment()); + assertNull(result.getTotalChargeAdjustmentReversed()); + assertNull(result.getTotalCreditBalanceRefund()); + assertNull(result.getTotalCreditBalanceRefundReversed()); + assertNull(result.getTotalRepaymentTransaction()); + assertNull(result.getTotalRepaymentTransactionReversed()); + } + + @Test + void map_collection_coversAllFields() { + final WorkingCapitalLoanCollectionData source = fullCollection(); + + final WorkingCapitalLoanCollectionDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(3, result.getDelinquentDays()); + assertEquals("2024-02-01", result.getDelinquentDate()); + assertEquals(new BigDecimal("100"), result.getDelinquentAmount()); + + // delinquentPrincipal is exposed as totalDelinquentAmount + assertEquals(new BigDecimal("80"), result.getTotalDelinquentAmount()); + + assertNotNull(result.getDelinquencyPausePeriods()); + assertEquals(1, result.getDelinquencyPausePeriods().size()); + assertEquals(Boolean.TRUE, result.getDelinquencyPausePeriods().getFirst().getActive()); + assertEquals("2024-01-10", result.getDelinquencyPausePeriods().getFirst().getPausePeriodStart()); + assertEquals("2024-01-20", result.getDelinquencyPausePeriods().getFirst().getPausePeriodEnd()); + + // serializer-only fields stay unmapped + assertNull(result.getLastPaymentDate()); + assertNull(result.getLastPaymentAmount()); + assertNull(result.getLastRepaymentDate()); + assertNull(result.getLastRepaymentAmount()); + assertNull(result.getDelinquencySchedule()); + } + + @Test + void map_delinquencySchedulePeriod_coversAllFields() { + final WorkingCapitalLoanDelinquencyRangeScheduleData source = fullSchedulePeriod(); + + final WorkingCapitalLoanDelinquencySchedulePeriodDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(2, result.getPeriodNumber()); + assertEquals("2024-01-01", result.getFromDate()); + assertEquals("2024-01-30", result.getToDate()); + assertEquals(new BigDecimal("150.00"), result.getExpectedAmount()); + assertEquals(new BigDecimal("50.00"), result.getPaidAmount()); + assertEquals(new BigDecimal("100.00"), result.getOutstandingAmount()); + assertEquals(Boolean.FALSE, result.getMinPaymentCriteriaMet()); + assertEquals(12, result.getDelinquentDays()); + assertEquals(new BigDecimal("100.00"), result.getDelinquentAmount()); + } + + @Test + void mapDelinquencySchedule_mapsEveryPeriod() { + final List result = mapper + .mapDelinquencySchedule(List.of(fullSchedulePeriod(), fullSchedulePeriod())); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(2, result.getFirst().getPeriodNumber()); + } + + @Test + void map_breachSchedulePeriod_coversAllFields() { + final WorkingCapitalLoanBreachScheduleData source = fullBreachSchedulePeriod(); + + final WorkingCapitalLoanBreachSchedulePeriodDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(4, result.getPeriodNumber()); + assertEquals("2024-02-01", result.getFromDate()); + assertEquals("2024-03-01", result.getToDate()); + assertEquals(29, result.getNumberOfDays()); + assertEquals(new BigDecimal("120.00"), result.getMinPaymentAmount()); + assertEquals(new BigDecimal("70.00"), result.getOutstandingAmount()); + assertEquals(Boolean.TRUE, result.getNearBreach()); + assertEquals(Boolean.FALSE, result.getBreach()); + assertEquals(Boolean.FALSE, result.getReset()); + } + + @Test + void mapBreachSchedule_mapsEveryPeriod() { + final List result = mapper + .mapBreachSchedule(List.of(fullBreachSchedulePeriod(), fullBreachSchedulePeriod())); + + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(4, result.getFirst().getPeriodNumber()); + } + + @Test + void map_charge_coversAllFields() { + final WorkingCapitalLoanChargeData source = fullCharge(); + + final WorkingCapitalLoanChargeDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(7L, result.getId()); + assertEquals(11L, result.getChargeId()); + assertEquals("Processing fee", result.getName()); + assertEquals("2024-03-01", result.getSubmittedOnDate()); + assertEquals("2024-03-02", result.getDueDate()); + assertEquals(new BigDecimal("25.00"), result.getAmount()); + assertEquals(new BigDecimal("10.00"), result.getAmountPaid()); + assertEquals(new BigDecimal("15.00"), result.getAmountOutstanding()); + assertEquals(Boolean.FALSE, result.getPenalty()); + assertEquals(Boolean.FALSE, result.getPaid()); + assertEquals(99L, result.getLoanId()); + assertEquals("charge-ext", result.getExternalId()); + assertEquals("loan-ext", result.getExternalLoanId()); + assertEquals(1, result.getChargeTimeType().getId()); + assertEquals("chargeTimeType.disbursement", result.getChargeTimeType().getCode()); + assertEquals("Disbursement", result.getChargeTimeType().getValue()); + assertEquals(2, result.getChargeCalculationType().getId()); + assertEquals("chargeCalculationType.flat", result.getChargeCalculationType().getCode()); + assertEquals("Flat", result.getChargeCalculationType().getValue()); + assertEquals(3, result.getChargePaymentMode().getId()); + assertEquals("chargePaymentMode.regular", result.getChargePaymentMode().getCode()); + assertEquals("Regular", result.getChargePaymentMode().getValue()); + + assertCurrency(source.getCurrency(), result.getCurrency()); + + // accrual fields are intentionally not mapped for working capital charges + assertNull(result.getAmountAccrued()); + assertNull(result.getAmountUnrecognized()); + + // write-off is not implemented and custom data is never filled for the loan-details event + assertNull(result.getAmountWrittenOff()); + assertNull(result.getCustomData()); + } + + @Test + void map_disbursementDetail_coversAllFields() { + final WorkingCapitalLoanDisbursementDetailData source = fullDisbursement(); + + final WorkingCapitalLoanDisbursementDetailDataV1 result = mapper.map(source); + + assertNotNull(result); + assertEquals(42, result.getId()); + assertEquals(new BigDecimal("500.00"), result.getPrincipal()); + assertEquals("2024-01-01", result.getExpectedDisbursementDate()); + assertEquals("2024-01-02", result.getActualDisbursementDate()); + + // netDisbursalAmount is intentionally not mapped + assertNull(result.getNetDisbursalAmount()); + } + + @Test + void toAvroDecimalScale_nullReturnsNull_valueRescaledToEightHalfUp() { + assertNull(mapper.toAvroDecimalScale(null)); + + final BigDecimal scaled = mapper.toAvroDecimalScale(new BigDecimal("0.123456785")); + assertEquals(8, scaled.scale()); + assertEquals(new BigDecimal("0.12345679"), scaled); + } + + private static void assertStringEnum(final StringEnumOptionData source, final StringEnumOptionDataV1 result) { + assertNotNull(result); + assertEquals(source.getId(), result.getId()); + assertEquals(source.getCode(), result.getCode()); + assertEquals(source.getValue(), result.getValue()); + } + + private static void assertCurrency(final CurrencyData source, final CurrencyDataV1 result) { + assertNotNull(result); + assertEquals(source.getCode(), result.getCode()); + assertEquals(source.getName(), result.getName()); + assertEquals(source.getDecimalPlaces(), result.getDecimalPlaces()); + assertEquals(source.getInMultiplesOf(), result.getInMultiplesOf()); + assertEquals(source.getDisplaySymbol(), result.getDisplaySymbol()); + assertEquals(source.getNameCode(), result.getNameCode()); + assertEquals(source.getDisplayLabel(), result.getDisplayLabel()); + } + + private static CurrencyData currency() { + return new CurrencyData("USD", "US Dollar", 2, 1, "$", "currency.USD"); + } + + private static StringEnumOptionData stringEnum(final String id, final String code, final String value) { + return new StringEnumOptionData(id, code, value); + } + + private static WorkingCapitalLoanData fullLoanData() { + return WorkingCapitalLoanData.builder().id(101L).accountNo("WC-000101").externalId(new ExternalId("ext-101")).clientId(11L) + .clientAccountNo("CL-11").clientName("John Doe").clientExternalId(new ExternalId("client-ext")).clientOfficeId(22L) + .fundId(44L).fundName("Main Fund").loanProductId(33L).loanProductName("WC Product") + .loanProductDescription("WC product description").status(fullStatus()).proposedPrincipal(new BigDecimal("1200.00")) + .approvedPrincipal(new BigDecimal("1100.00")).principal(new BigDecimal("1000.00")) + .netDisbursalAmount(new BigDecimal("980.00")) + .amortizationType(stringEnum("1", "amortizationType.equal.installments", "Equal installments")).npvDayCount(360) + .loanProductCounter(7).paymentRate(new BigDecimal("2.50")).repaymentEvery(30) + .repaymentFrequencyType(stringEnum("0", "repaymentFrequency.days", "Days")).discountFee(new BigDecimal("12.00")) + .proposedDiscountFee(new BigDecimal("13.00")).approvedDiscountFee(new BigDecimal("14.00")).numberOfRepayments(90) + .periodPaymentAmount(new BigDecimal("120.00")).dailyEir(new BigDecimal("0.05")).calculatedAnnualEir(new BigDecimal("18.25")) + .totalPaymentVolume(new BigDecimal("5000.00")).breachGraceDays(3).delinquencyGraceDays(7) + .delinquencyStartType(stringEnum("1", "delinquencyStart.disbursement", "Disbursement")) + .delinquencyStartDate(LocalDate.of(2024, 1, 5)).breachStartDate(LocalDate.of(2024, 1, 6)) + .lastClosedBusinessDate(LocalDate.of(2024, 2, 1)).chargedOff(Boolean.TRUE).enableInstallmentLevelDelinquency(Boolean.TRUE) + .currency(currency()).timeline(fullTimeline()).summary(fullSummary()).delinquent(fullCollection()).breach(fullBreach()) + .nearBreach(fullNearBreach()).charges(List.of(fullCharge())).disbursementDetails(List.of(fullDisbursement())) + .originators(List.of(fullOriginator())).build(); + } + + private static LoanStatusEnumData fullStatus() { + return new LoanStatusEnumData(300L, "loanStatusType.active", "Active"); + } + + private static LoanApplicationTimelineData fullTimeline() { + return new LoanApplicationTimelineData().setSubmittedOnDate(LocalDate.of(2024, 1, 1)).setSubmittedByUsername("sub-user") + .setSubmittedByFirstname("sub-first").setSubmittedByLastname("sub-last").setRejectedOnDate(LocalDate.of(2024, 1, 2)) + .setRejectedByUsername("rej-user").setRejectedByFirstname("rej-first").setRejectedByLastname("rej-last") + .setWithdrawnOnDate(LocalDate.of(2024, 1, 3)).setWithdrawnByUsername("wd-user").setWithdrawnByFirstname("wd-first") + .setWithdrawnByLastname("wd-last").setApprovedOnDate(LocalDate.of(2024, 1, 4)).setApprovedByUsername("app-user") + .setApprovedByFirstname("app-first").setApprovedByLastname("app-last").setExpectedDisbursementDate(LocalDate.of(2024, 1, 5)) + .setActualDisbursementDate(LocalDate.of(2024, 1, 6)).setDisbursedByUsername("dis-user").setDisbursedByFirstname("dis-first") + .setDisbursedByLastname("dis-last").setClosedOnDate(LocalDate.of(2024, 1, 7)).setClosedByUsername("cl-user") + .setClosedByFirstname("cl-first").setClosedByLastname("cl-last").setExpectedMaturityDate(LocalDate.of(2024, 1, 8)) + .setWriteOffOnDate(LocalDate.of(2024, 1, 9)).setWriteOffByUsername("wo-user").setWriteOffByFirstname("wo-first") + .setWriteOffByLastname("wo-last").setChargedOffOnDate(LocalDate.of(2024, 1, 10)).setChargedOffByUsername("co-user") + .setChargedOffByFirstname("co-first").setChargedOffByLastname("co-last"); + } + + private static WorkingCapitalLoanSummaryData fullSummary() { + return WorkingCapitalLoanSummaryData.builder().currency(currency()).principal(new BigDecimal("1000")) + .principalPaid(new BigDecimal("100")).principalOutstanding(new BigDecimal("900")).fee(new BigDecimal("50")) + .feePaid(new BigDecimal("20")).feeOutstanding(new BigDecimal("30")).penalty(new BigDecimal("10")) + .penaltyPaid(new BigDecimal("4")).penaltyOutstanding(new BigDecimal("6")) + .realizedIncomeFromDiscountFee(new BigDecimal("15")).unrealizedIncomeFromDiscountFee(new BigDecimal("35")) + .overpayment(new BigDecimal("9.99")).totalDisbursement(new BigDecimal("950")).totalDiscountFee(new BigDecimal("50")) + .totalDiscountFeeAdjustment(new BigDecimal("5")).totalExpectedRepayment(new BigDecimal("1500")) + .totalRepayment(new BigDecimal("600")).totalOutstanding(new BigDecimal("900")).build(); + } + + private static WorkingCapitalLoanCollectionData fullCollection() { + final DelinquencyPausePeriod pause = new DelinquencyPausePeriod(true, LocalDate.of(2024, 1, 10), LocalDate.of(2024, 1, 20)); + return new WorkingCapitalLoanCollectionData(5L, 3L, LocalDate.of(2024, 2, 1), new BigDecimal("100"), List.of(pause), List.of(), + new BigDecimal("80")); + } + + private static WorkingCapitalLoanDelinquencyRangeScheduleData fullSchedulePeriod() { + return new WorkingCapitalLoanDelinquencyRangeScheduleData(9L, 101L, 2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 30), + new BigDecimal("150.00"), new BigDecimal("50.00"), new BigDecimal("100.00"), Boolean.FALSE, 12L, new BigDecimal("100.00")); + } + + private static WorkingCapitalLoanBreachScheduleData fullBreachSchedulePeriod() { + return new WorkingCapitalLoanBreachScheduleData(15L, 101L, 4, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), 29, + new BigDecimal("120.00"), new BigDecimal("70.00"), Boolean.TRUE, Boolean.FALSE, Boolean.FALSE); + } + + private static WorkingCapitalLoanChargeData fullCharge() { + return WorkingCapitalLoanChargeData.builder().id(7L).chargeId(11L).name("Processing fee") + .chargeTimeType(new EnumOptionData(1L, "chargeTimeType.disbursement", "Disbursement")) + .submittedOnDate(LocalDate.of(2024, 3, 1)).dueDate(LocalDate.of(2024, 3, 2)) + .chargeCalculationType(new EnumOptionData(2L, "chargeCalculationType.flat", "Flat")).currency(currency()) + .amount(new BigDecimal("25.00")).amountPaid(new BigDecimal("10.00")).amountOutstanding(new BigDecimal("15.00")) + .penalty(false).chargePaymentMode(new EnumOptionData(3L, "chargePaymentMode.regular", "Regular")).paid(false).loanId(99L) + .externalId(new ExternalId("charge-ext")).externalLoanId(new ExternalId("loan-ext")).build(); + } + + private static WorkingCapitalLoanDisbursementDetailData fullDisbursement() { + return WorkingCapitalLoanDisbursementDetailData.builder().id(42L).principal(new BigDecimal("500.00")) + .expectedDisbursementDate(LocalDate.of(2024, 1, 1)).actualDisbursementDate(LocalDate.of(2024, 1, 2)).build(); + } + + private static WorkingCapitalBreachData fullBreach() { + return WorkingCapitalBreachData.builder().id(80L).name("Breach rule").breachFrequency(2) + .breachFrequencyType(stringEnum("1", "breachFrequency.days", "Days")) + .breachAmountCalculationType(stringEnum("1", "breachAmount.fixed", "Fixed")).breachAmount(new BigDecimal("500.00")).build(); + } + + private static WorkingCapitalNearBreachData fullNearBreach() { + return WorkingCapitalNearBreachData.builder().id(81L).name("Near breach rule").frequency(1) + .frequencyType(stringEnum("1", "nearBreachFrequency.days", "Days")).threshold(new BigDecimal("450.00")).build(); + } + + private static LoanOriginatorData fullOriginator() { + final CodeValueData originatorType = CodeValueData.instance(71L, "Broker", "originator type", 1, true, false); + final CodeValueData channelType = CodeValueData.instance(72L, "Online", "channel type", 2, true, false); + return LoanOriginatorData.builder().id(70L).externalId("orig-ext").name("Origin Bank").status("ACTIVE") + .originatorType(originatorType).channelType(channelType).build(); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java index a97e9fc8184..303ac43ad45 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java @@ -715,6 +715,56 @@ public static ArrayList> getDefaultExternalEventConfiguratio workingCapitalLoanChargeAdjustmentPostBusinessEvent.put("enabled", false); defaults.add(workingCapitalLoanChargeAdjustmentPostBusinessEvent); + Map workingCapitalLoanCreatedBusinessEvent = new HashMap<>(); + workingCapitalLoanCreatedBusinessEvent.put("type", "WorkingCapitalLoanCreatedBusinessEvent"); + workingCapitalLoanCreatedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanCreatedBusinessEvent); + + Map workingCapitalLoanApplicationModifiedBusinessEvent = new HashMap<>(); + workingCapitalLoanApplicationModifiedBusinessEvent.put("type", "WorkingCapitalLoanApplicationModifiedBusinessEvent"); + workingCapitalLoanApplicationModifiedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanApplicationModifiedBusinessEvent); + + Map workingCapitalLoanApprovedBusinessEvent = new HashMap<>(); + workingCapitalLoanApprovedBusinessEvent.put("type", "WorkingCapitalLoanApprovedBusinessEvent"); + workingCapitalLoanApprovedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanApprovedBusinessEvent); + + Map workingCapitalLoanUndoApprovalBusinessEvent = new HashMap<>(); + workingCapitalLoanUndoApprovalBusinessEvent.put("type", "WorkingCapitalLoanUndoApprovalBusinessEvent"); + workingCapitalLoanUndoApprovalBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanUndoApprovalBusinessEvent); + + Map workingCapitalLoanRejectedBusinessEvent = new HashMap<>(); + workingCapitalLoanRejectedBusinessEvent.put("type", "WorkingCapitalLoanRejectedBusinessEvent"); + workingCapitalLoanRejectedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanRejectedBusinessEvent); + + Map workingCapitalLoanDisbursalBusinessEvent = new HashMap<>(); + workingCapitalLoanDisbursalBusinessEvent.put("type", "WorkingCapitalLoanDisbursalBusinessEvent"); + workingCapitalLoanDisbursalBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanDisbursalBusinessEvent); + + Map workingCapitalLoanUndoDisbursalBusinessEvent = new HashMap<>(); + workingCapitalLoanUndoDisbursalBusinessEvent.put("type", "WorkingCapitalLoanUndoDisbursalBusinessEvent"); + workingCapitalLoanUndoDisbursalBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanUndoDisbursalBusinessEvent); + + Map workingCapitalLoanStatusChangedBusinessEvent = new HashMap<>(); + workingCapitalLoanStatusChangedBusinessEvent.put("type", "WorkingCapitalLoanStatusChangedBusinessEvent"); + workingCapitalLoanStatusChangedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanStatusChangedBusinessEvent); + + Map workingCapitalLoanBalanceChangedBusinessEvent = new HashMap<>(); + workingCapitalLoanBalanceChangedBusinessEvent.put("type", "WorkingCapitalLoanBalanceChangedBusinessEvent"); + workingCapitalLoanBalanceChangedBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanBalanceChangedBusinessEvent); + + Map workingCapitalLoanDelinquencyRangeChangeBusinessEvent = new HashMap<>(); + workingCapitalLoanDelinquencyRangeChangeBusinessEvent.put("type", "WorkingCapitalLoanDelinquencyRangeChangeBusinessEvent"); + workingCapitalLoanDelinquencyRangeChangeBusinessEvent.put("enabled", false); + defaults.add(workingCapitalLoanDelinquencyRangeChangeBusinessEvent); + return defaults; }